mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +00:00
Run SwiftFormat.
This commit is contained in:
@@ -98,12 +98,12 @@ class TimelineInteractionHandler {
|
||||
// Don't show a menu for non-event based items.
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
actionsSubject.send(.composer(action: .removeFocus))
|
||||
actionsSubject.send(.showActionMenu(.init(item: eventTimelineItem)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// swiftlint:disable:next cyclomatic_complexity
|
||||
func handleTimelineItemMenuAction(_ action: TimelineItemMenuAction, itemID: TimelineItemIdentifier) {
|
||||
guard let timelineItem = timelineController.timelineItems.firstUsingStableID(itemID),
|
||||
@@ -250,7 +250,7 @@ class TimelineInteractionHandler {
|
||||
}
|
||||
|
||||
// MARK: Polls
|
||||
|
||||
|
||||
func sendPollResponse(pollStartID: String, optionID: String) {
|
||||
Task {
|
||||
let sendPollResponseResult = await pollInteractionHandler.sendPollResponse(pollStartID: pollStartID, optionID: optionID)
|
||||
@@ -369,7 +369,7 @@ class TimelineInteractionHandler {
|
||||
isReply: false,
|
||||
messageType: .VoiceMessage,
|
||||
startsThread: nil)
|
||||
|
||||
|
||||
actionsSubject.send(.composer(action: .setMode(mode: .previewVoiceMessage(state: audioPlayerState, waveform: .url(recordingURL), isUploading: true))))
|
||||
await voiceMessageRecorder.stopPlayback()
|
||||
|
||||
@@ -417,13 +417,13 @@ class TimelineInteractionHandler {
|
||||
}
|
||||
|
||||
// MARK: Audio Playback
|
||||
|
||||
|
||||
func changePlaybackSpeed(for itemID: TimelineItemIdentifier) {
|
||||
let nextSpeed = appSettings.voiceMessagePlaybackSpeed.next
|
||||
appSettings.voiceMessagePlaybackSpeed = nextSpeed
|
||||
audioPlayerState(for: itemID)?.setPlaybackSpeed(nextSpeed)
|
||||
}
|
||||
|
||||
|
||||
func playPauseAudio(for itemID: TimelineItemIdentifier) async {
|
||||
MXLog.info("Toggle play/pause audio for itemID \(itemID)")
|
||||
guard let timelineItem = timelineController.timelineItems.firstUsingStableID(itemID) else {
|
||||
@@ -440,12 +440,12 @@ class TimelineInteractionHandler {
|
||||
}
|
||||
|
||||
let audioPlayer = mediaPlayerProvider.player
|
||||
|
||||
|
||||
// Stop any recording in progress
|
||||
if voiceMessageRecorder.isRecording {
|
||||
await voiceMessageRecorder.stopRecording()
|
||||
}
|
||||
|
||||
|
||||
guard let audioPlayerState = audioPlayerState(for: itemID) else {
|
||||
fatalError("Audio player state not found for \(itemID)")
|
||||
}
|
||||
@@ -454,16 +454,16 @@ class TimelineInteractionHandler {
|
||||
if !audioPlayerState.isAttached {
|
||||
audioPlayerState.attachAudioPlayer(audioPlayer)
|
||||
}
|
||||
|
||||
|
||||
// Detach all other states
|
||||
await mediaPlayerProvider.detachAllStates(except: audioPlayerState)
|
||||
|
||||
|
||||
guard audioPlayer.sourceURL == source.url, audioPlayer.state != .error else {
|
||||
// Load content
|
||||
do {
|
||||
MXLog.info("Loading voice message audio content from source for itemID \(itemID)")
|
||||
let url = try await userSession.voiceMessageMediaManager.loadVoiceMessageFromSource(source, body: nil)
|
||||
|
||||
|
||||
// Make sure that the player is still attached, as it may have been detached while waiting for the voice message to be loaded.
|
||||
if audioPlayerState.isAttached {
|
||||
audioPlayer.load(sourceURL: source.url, playbackURL: url, autoplay: true)
|
||||
@@ -482,7 +482,7 @@ class TimelineInteractionHandler {
|
||||
audioPlayer.play()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func seekAudio(for itemID: TimelineItemIdentifier, progress: Double) async {
|
||||
guard let playerState = mediaPlayerProvider.playerState(for: .timelineItemIdentifier(itemID)) else {
|
||||
return
|
||||
|
||||
@@ -108,7 +108,7 @@ struct TimelineViewState: BindableState {
|
||||
var showReadReceipts = false
|
||||
var isDM = false
|
||||
var timelineState: TimelineState // check the doc before changing this
|
||||
|
||||
|
||||
var ownUserID: String
|
||||
var canCurrentUserSendMessage = false
|
||||
var canCurrentUserRedactOthers = false
|
||||
@@ -125,7 +125,7 @@ struct TimelineViewState: BindableState {
|
||||
var jumpToReadMarkerEnabled: Bool
|
||||
|
||||
let hasPredecessor: Bool
|
||||
|
||||
|
||||
/// The `pinnedEventIDs` are used only to determine if an item is already pinned or not.
|
||||
/// It's updated from the room info, so it's faster than using the timeline
|
||||
var pinnedEventIDs: Set<String> = []
|
||||
@@ -147,24 +147,24 @@ struct TimelineViewState: BindableState {
|
||||
var linkMetadataProvider: LinkMetadataProviderProtocol?
|
||||
|
||||
var mapTilerConfiguration: MapTilerConfiguration
|
||||
|
||||
|
||||
var stoppedLiveLocationIDs: Set<TimelineItemIdentifier> = []
|
||||
|
||||
|
||||
var bindings: TimelineViewStateBindings
|
||||
}
|
||||
|
||||
struct TimelineViewStateBindings {
|
||||
var isScrolledToBottom = true
|
||||
|
||||
|
||||
/// Whether the read marker (NEW banner) is currently visible in the timeline viewport.
|
||||
/// Used to hide the jump-to-unread button once the user has scrolled to the read marker.
|
||||
var isReadMarkerVisible = false
|
||||
|
||||
|
||||
/// Whether new messages have arrived while the user is scrolled away from the bottom of a
|
||||
/// live timeline. Drives the presence dot on the scroll-to-bottom button and resets when the
|
||||
/// user returns to the bottom.
|
||||
var hasNewMessagesAtBottom = false
|
||||
|
||||
|
||||
/// The timestamp of the topmost visible item, used to drive the floating date badge while scrolling.
|
||||
var floatingDate: Date?
|
||||
|
||||
@@ -183,7 +183,7 @@ struct TimelineViewStateBindings {
|
||||
var readReceiptsSummaryInfo: ReadReceiptSummaryInfo?
|
||||
|
||||
var manageMemberViewModel: ManageRoomMemberSheetViewModel?
|
||||
|
||||
|
||||
var showTranslation = false
|
||||
var textToBeTranslated: String?
|
||||
}
|
||||
@@ -248,7 +248,7 @@ struct TimelineState {
|
||||
/// The event has already been shown.
|
||||
case hasAppeared
|
||||
}
|
||||
|
||||
|
||||
/// The ID of the event.
|
||||
let eventID: String
|
||||
/// How the event should be shown, or whether it has already appeared.
|
||||
@@ -262,26 +262,26 @@ struct TimelineState {
|
||||
var scrollToBottomPublisher = PassthroughSubject<Void, Never>()
|
||||
var scrollToFirstItemForDatePublisher = PassthroughSubject<Void, Never>()
|
||||
var scrollToReadMarkerPublisher = PassthroughSubject<TimelineItemIdentifier.UniqueID, Never>()
|
||||
|
||||
|
||||
var itemsDictionary = OrderedDictionary<TimelineItemIdentifier.UniqueID, RoomTimelineItemViewState>()
|
||||
|
||||
|
||||
var uniqueIDs: [TimelineItemIdentifier.UniqueID] {
|
||||
itemsDictionary.keys.elements
|
||||
}
|
||||
|
||||
|
||||
var itemViewStates: [RoomTimelineItemViewState] {
|
||||
itemsDictionary.values.elements
|
||||
}
|
||||
|
||||
|
||||
/// The unique ID of the read marker (NEW banner) in the timeline, if present.
|
||||
/// Recomputed by ``recomputeReadMarkerUniqueID()`` whenever ``itemsDictionary`` changes.
|
||||
private(set) var readMarkerUniqueID: TimelineItemIdentifier.UniqueID?
|
||||
|
||||
|
||||
/// The user's `m.fully_read` event ID, pushed from `RoomInfo`. Used as a fallback
|
||||
/// signal when ``readMarkerUniqueID`` is nil because the marker event isn't paginated
|
||||
/// into the loaded timeline window.
|
||||
var fullyReadEventID: String?
|
||||
|
||||
|
||||
/// Recomputes ``readMarkerUniqueID`` from ``itemsDictionary``. Call after assigning a new
|
||||
/// value to ``itemsDictionary``.
|
||||
mutating func recomputeReadMarkerUniqueID() {
|
||||
@@ -290,7 +290,7 @@ struct TimelineState {
|
||||
return false
|
||||
}?.key
|
||||
}
|
||||
|
||||
|
||||
func hasLoadedItem(with eventID: String) -> Bool {
|
||||
itemViewStates.contains { $0.identifier.eventID == eventID }
|
||||
}
|
||||
@@ -306,13 +306,13 @@ extension TimelineViewState {
|
||||
var isAtBottomAndLive: Bool {
|
||||
bindings.isScrolledToBottom && timelineState.isLive
|
||||
}
|
||||
|
||||
|
||||
/// Whether the scroll-to-bottom button should be shown: the user is scrolled away from the
|
||||
/// bottom of a live timeline.
|
||||
var shouldShowScrollToBottomButton: Bool {
|
||||
!isAtBottomAndLive
|
||||
}
|
||||
|
||||
|
||||
/// Whether the jump-to-read-marker button should be shown.
|
||||
///
|
||||
/// Primary path: the SDK has materialised a virtual `ReadMarker` item in the
|
||||
@@ -334,7 +334,7 @@ extension TimelineViewState {
|
||||
return !timelineState.itemsDictionary.isEmpty
|
||||
&& !timelineState.hasLoadedItem(with: fullyReadEventID)
|
||||
}
|
||||
|
||||
|
||||
/// The string shown as the message preview.
|
||||
///
|
||||
/// This converts the formatted body to a plain string to remove formatting
|
||||
|
||||
@@ -148,14 +148,14 @@ class TimelineTableViewController: UIViewController {
|
||||
@Binding private var isReadMarkerVisible: Bool
|
||||
@Binding private var hasNewMessagesAtBottom: Bool
|
||||
@Binding private var floatingDate: Date?
|
||||
|
||||
|
||||
/// The unique ID of the read marker (NEW banner) currently in the timeline, if any.
|
||||
/// Updated by `TimelineViewRepresentable.updateUIViewController` whenever it changes.
|
||||
var readMarkerUniqueID: TimelineItemIdentifier.UniqueID?
|
||||
|
||||
|
||||
/// A work item used to auto-hide the floating date badge after scrolling stops.
|
||||
private var floatingDateHideWorkItem: DispatchWorkItem?
|
||||
|
||||
|
||||
private var timelineItemsIDs: [TimelineItemIdentifier.UniqueID] {
|
||||
timelineItemsDictionary.keys.elements.reversed()
|
||||
}
|
||||
@@ -163,7 +163,7 @@ class TimelineTableViewController: UIViewController {
|
||||
/// The table's diffable data source.
|
||||
private var dataSource: UITableViewDiffableDataSource<TimelineSection, TimelineItemIdentifier.UniqueID>?
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
|
||||
/// A publisher used to throttle back pagination requests.
|
||||
///
|
||||
/// Our view actions get wrapped in a `Task` so it is possible that a second call in
|
||||
@@ -196,7 +196,7 @@ class TimelineTableViewController: UIViewController {
|
||||
_isReadMarkerVisible = isReadMarkerVisible
|
||||
_hasNewMessagesAtBottom = hasNewMessagesAtBottom
|
||||
_floatingDate = floatingDate
|
||||
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
tableView.register(TimelineItemCell.self, forCellReuseIdentifier: TimelineItemCell.reuseIdentifier)
|
||||
@@ -226,7 +226,7 @@ class TimelineTableViewController: UIViewController {
|
||||
self?.scrollToFirstItemForCurrentDate()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
|
||||
scrollToReadMarkerPublisher
|
||||
.sink { [weak self] uniqueID in
|
||||
self?.scrollToItem(uniqueID: uniqueID, animated: true)
|
||||
@@ -362,7 +362,7 @@ class TimelineTableViewController: UIViewController {
|
||||
/// the scroll position will be updated to maintain the position of the last visible item.
|
||||
private func applySnapshot() {
|
||||
guard let dataSource else { return }
|
||||
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<TimelineSection, TimelineItemIdentifier.UniqueID>()
|
||||
|
||||
// We don't want to display the typing notification in this timeline
|
||||
@@ -400,13 +400,13 @@ class TimelineTableViewController: UIViewController {
|
||||
if isSwitchingTimelines {
|
||||
coordinator.send(viewAction: .hasSwitchedTimeline)
|
||||
}
|
||||
|
||||
|
||||
// Re-evaluate after the snapshot has been applied so the new layout is reflected.
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.updateReadMarkerVisibility()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Scrolls to the newest item in the timeline.
|
||||
private func scrollToNewestItem(animated: Bool) {
|
||||
guard !timelineItemsIDs.isEmpty else {
|
||||
@@ -415,7 +415,7 @@ class TimelineTableViewController: UIViewController {
|
||||
tableView.scrollToRow(at: IndexPath(item: 0, section: 0), at: .top, animated: animated)
|
||||
scrollDirectionPublisher.send(.bottom)
|
||||
}
|
||||
|
||||
|
||||
/// Scrolls to the oldest item in the timeline.
|
||||
private func scrollToOldestItem(animated: Bool) {
|
||||
guard !timelineItemsIDs.isEmpty else {
|
||||
@@ -434,7 +434,7 @@ class TimelineTableViewController: UIViewController {
|
||||
tableView.scrollToRow(at: indexPath, at: .middle, animated: animated)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Scrolls to the item with the corresponding event ID if loaded in the timeline.
|
||||
private func scrollToItem(eventID: String, animated: Bool) {
|
||||
DispatchQueue.main.async { [weak self] in // Fixes #2805
|
||||
@@ -488,12 +488,12 @@ class TimelineTableViewController: UIViewController {
|
||||
}
|
||||
return readMarkerIndexPath <= maxVisibleIndexPath
|
||||
}()
|
||||
|
||||
|
||||
if isReadMarkerVisible != isVisible {
|
||||
isReadMarkerVisible = isVisible
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func sendLastVisibleItemReadReceipt() {
|
||||
// Find the last visible timeline item and send a read receipt for it
|
||||
guard let visibleIndexPaths = tableView.indexPathsForVisibleRows else {
|
||||
@@ -520,9 +520,9 @@ extension TimelineTableViewController: UITableViewDelegate {
|
||||
// Dispatch to fix runtime warning about making changes during a view update.
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self else { return }
|
||||
|
||||
|
||||
let isScrolledToBottom = scrollView.contentOffset.y <= 0
|
||||
|
||||
|
||||
// Only update the binding on changes to avoid needlessly recomputing the hierarchy when scrolling.
|
||||
if self.isScrolledToBottom != isScrolledToBottom {
|
||||
self.isScrolledToBottom = isScrolledToBottom
|
||||
@@ -530,14 +530,14 @@ extension TimelineTableViewController: UITableViewDelegate {
|
||||
self.hasNewMessagesAtBottom = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if !isScrolledToBottom {
|
||||
updateFloatingDate()
|
||||
}
|
||||
|
||||
|
||||
updateReadMarkerVisibility()
|
||||
}
|
||||
|
||||
|
||||
// We never want the table view to be fully at the bottom to allow the status bar tap to work properly
|
||||
if scrollView.contentOffset.y == 0 {
|
||||
scrollView.contentOffset.y = -1
|
||||
|
||||
@@ -21,7 +21,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
static let focusTimelineToastIndicatorID = "RoomScreenFocusTimelineToastIndicator"
|
||||
static let toastErrorID = "RoomScreenToastError"
|
||||
}
|
||||
|
||||
|
||||
private let roomProxy: JoinedRoomProxyProtocol
|
||||
private let timelineController: TimelineControllerProtocol
|
||||
private let userSession: UserSessionProtocol
|
||||
@@ -46,7 +46,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
|
||||
private var paginateBackwardsTask: Task<Void, Never>?
|
||||
private var paginateForwardsTask: Task<Void, Never>?
|
||||
|
||||
|
||||
init(roomProxy: JoinedRoomProxyProtocol,
|
||||
focussedEventID: String? = nil,
|
||||
timelineController: TimelineControllerProtocol,
|
||||
@@ -144,7 +144,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
|
||||
updateRoomInfo(roomProxy.infoPublisher.value)
|
||||
updateMembers(roomProxy.membersPublisher.value)
|
||||
|
||||
|
||||
// Note: beware if we get to e.g. restore a reply / edit,
|
||||
// maybe we are tracking a non-needed first initial state
|
||||
trackComposerMode(.default)
|
||||
@@ -233,7 +233,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
actionsSubject.send(.displayRoom(roomID: predecessorID, via: Array(serverNames)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func process(composerAction: ComposerToolbarViewModelAction) {
|
||||
switch composerAction {
|
||||
case .sendMessage(let message, let html, let mode, let intentionalMentions):
|
||||
@@ -451,7 +451,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] callback in
|
||||
guard let self else { return }
|
||||
|
||||
|
||||
switch callback {
|
||||
case .updatedTimelineItems(let updatedItems, let isSwitchingTimelines):
|
||||
buildTimelineViews(timelineItems: updatedItems, isSwitchingTimelines: isSwitchingTimelines)
|
||||
@@ -477,7 +477,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
|
||||
roomProxy.infoPublisher
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] roomInfo in
|
||||
@@ -540,7 +540,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
|
||||
func viewInRoomTimeline(eventID: String) async {
|
||||
switch await roomProxy.loadOrFetchEventDetails(for: eventID) {
|
||||
case .success(let event):
|
||||
@@ -567,7 +567,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
appSettings.$threadsEnabled
|
||||
.weakAssign(to: \.state.areThreadsEnabled, on: self)
|
||||
.store(in: &cancellables)
|
||||
|
||||
|
||||
appSettings.$jumpToReadMarkerEnabled
|
||||
.weakAssign(to: \.state.jumpToReadMarkerEnabled, on: self)
|
||||
.store(in: &cancellables)
|
||||
@@ -592,39 +592,39 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
.weakAssign(to: \.state.hideTimelineMedia, on: self)
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
|
||||
private func setupDirectRoomSubscriptionsIfNeeded() {
|
||||
guard roomProxy.infoPublisher.value.isDirect else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let shouldShowInviteAlert = composerFocusedSubject
|
||||
.removeDuplicates()
|
||||
.map { [weak self] isFocused in
|
||||
guard let self else { return false }
|
||||
|
||||
|
||||
return isFocused && self.roomProxy.infoPublisher.value.isUserAloneInDirectRoom
|
||||
}
|
||||
// We want to show the alert just once, so we are taking the first "true" emitted
|
||||
.first { $0 }
|
||||
|
||||
|
||||
shouldShowInviteAlert
|
||||
.sink { [weak self] _ in
|
||||
self?.displayAlert(.inviteAgain)
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
|
||||
|
||||
private func paginateBackwards() {
|
||||
guard paginateBackwardsTask == nil else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
paginateBackwardsTask = Task { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
switch await timelineController.paginateBackwards(requestSize: Constants.paginationEventLimit) {
|
||||
case .failure:
|
||||
displayErrorToast(L10n.errorFailedLoadingMessages)
|
||||
@@ -639,12 +639,12 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
guard paginateForwardsTask == nil else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
paginateForwardsTask = Task { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
switch await timelineController.paginateForwards(requestSize: Constants.paginationEventLimit) {
|
||||
case .failure:
|
||||
displayErrorToast(L10n.errorFailedLoadingMessages)
|
||||
@@ -667,20 +667,20 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
focusLive()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func scrollToReadMarker() async {
|
||||
// Primary: SDK has materialised the virtual ReadMarker. Smooth in-window scroll.
|
||||
if let readMarkerID = state.timelineState.readMarkerUniqueID {
|
||||
state.timelineState.scrollToReadMarkerPublisher.send(readMarkerID)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Fallback: mirror the same-room permalink flow exactly
|
||||
// (RoomFlowCoordinator.handleChildEventRoute lines 223-258): pre-fetch the
|
||||
// event first to prime the SDK's event cache, then focus on it via the
|
||||
// same focusOnEvent path permalinks use.
|
||||
guard let fullyReadEventID = state.timelineState.fullyReadEventID else { return }
|
||||
|
||||
|
||||
switch await roomProxy.loadOrFetchEventDetails(for: fullyReadEventID) {
|
||||
case .success:
|
||||
await focusOnEvent(eventID: fullyReadEventID)
|
||||
@@ -688,13 +688,13 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
displayErrorToast(L10n.errorMessageNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func sendReadReceiptIfNeeded(for lastVisibleItemID: TimelineItemIdentifier) async {
|
||||
guard appMediator.appState == .active else { return }
|
||||
|
||||
|
||||
await timelineController.sendReadReceipt(for: lastVisibleItemID)
|
||||
}
|
||||
|
||||
|
||||
private func handleMediaTapped(with itemID: TimelineItemIdentifier) async {
|
||||
state.showLoading = true
|
||||
let action = await timelineInteractionHandler.processItemTap(itemID)
|
||||
@@ -749,7 +749,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
private func handleJoinCommand(message: String) async {
|
||||
guard let alias = String(message.dropFirst(SlashCommand.join.rawValue.count))
|
||||
.components(separatedBy: .whitespacesAndNewlines)
|
||||
@@ -799,7 +799,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
|
||||
scrollToBottom()
|
||||
}
|
||||
|
||||
|
||||
private func trackComposerMode(_ mode: ComposerMode) {
|
||||
var isEdit = false
|
||||
var isReply = false
|
||||
@@ -846,7 +846,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
mediaPlayerProvider.unregister(audioPlayerState: playerState)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let itemsGroupedByTimelineDisplayStyle = timelineItems.chunked { current, next in
|
||||
canGroupItem(timelineItem: current, with: next)
|
||||
}
|
||||
@@ -881,13 +881,13 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
if isSwitchingTimelines {
|
||||
state.timelineState.isSwitchingTimelines = true
|
||||
}
|
||||
|
||||
|
||||
updateHasNewMessagesAtBottom(with: timelineItemsDictionary)
|
||||
|
||||
|
||||
state.timelineState.itemsDictionary = timelineItemsDictionary
|
||||
state.timelineState.recomputeReadMarkerUniqueID()
|
||||
}
|
||||
|
||||
|
||||
private func updateViewState(item: RoomTimelineItemProtocol, groupStyle: TimelineGroupStyle) -> RoomTimelineItemViewState {
|
||||
if let timelineItemViewState = state.timelineState.itemsDictionary[item.id.uniqueID] {
|
||||
timelineItemViewState.groupStyle = groupStyle
|
||||
@@ -897,7 +897,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
return RoomTimelineItemViewState(item: item, groupStyle: groupStyle)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func canGroupItem(timelineItem: RoomTimelineItemProtocol, with otherTimelineItem: RoomTimelineItemProtocol) -> Bool {
|
||||
if timelineItem is CollapsibleTimelineItem || otherTimelineItem is CollapsibleTimelineItem {
|
||||
return false
|
||||
@@ -917,7 +917,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
&& eventTimelineItem.properties.reactions.isEmpty // Reactions break the grouping.
|
||||
&& otherEventTimelineItem.timestamp.timeIntervalSince(eventTimelineItem.timestamp) < 5 * 60 // As does the passage of time.
|
||||
}
|
||||
|
||||
|
||||
/// Sets `hasNewMessagesAtBottom` to `true` when newer items arrive while the user is scrolled
|
||||
/// up in a live timeline. Skips initial load and timeline switches.
|
||||
private func updateHasNewMessagesAtBottom(with newTimelineItems: OrderedDictionary<TimelineItemIdentifier.UniqueID, RoomTimelineItemViewState>) {
|
||||
@@ -928,33 +928,33 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
!state.bindings.hasNewMessagesAtBottom else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let oldDictionary = state.timelineState.itemsDictionary
|
||||
guard !oldDictionary.isEmpty,
|
||||
!newTimelineItems.isEmpty,
|
||||
oldDictionary.keys.last != newTimelineItems.keys.last else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
state.bindings.hasNewMessagesAtBottom = true
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Direct chats logics
|
||||
|
||||
|
||||
private let inviteLoadingIndicatorID = UUID().uuidString
|
||||
|
||||
|
||||
private func inviteOtherDMUserBack() {
|
||||
guard roomProxy.infoPublisher.value.isUserAloneInDirectRoom else {
|
||||
displayAlert(.unknown)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Task {
|
||||
userIndicatorController.submitIndicator(.init(id: inviteLoadingIndicatorID, type: .toast, title: L10n.commonLoading))
|
||||
defer {
|
||||
userIndicatorController.retractIndicatorWithId(inviteLoadingIndicatorID)
|
||||
}
|
||||
|
||||
|
||||
guard
|
||||
let members = await roomProxy.members(),
|
||||
members.count == 2,
|
||||
@@ -963,7 +963,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
displayAlert(.unknown)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
switch await roomProxy.invite(userID: otherPerson.userID) {
|
||||
case .success:
|
||||
break
|
||||
@@ -974,7 +974,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
}
|
||||
|
||||
// MARK: - Reactions
|
||||
|
||||
|
||||
private func displayReactionSummary(for itemID: TimelineItemIdentifier, selectedKey: String) {
|
||||
guard let timelineItem = timelineController.timelineItems.firstUsingStableID(itemID),
|
||||
let eventTimelineItem = timelineItem as? EventBasedTimelineItemProtocol else {
|
||||
@@ -985,7 +985,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
}
|
||||
|
||||
// MARK: - Read Receipts
|
||||
|
||||
|
||||
private func displayReadReceipts(for itemID: TimelineItemIdentifier) {
|
||||
guard let timelineItem = timelineController.timelineItems.firstUsingStableID(itemID),
|
||||
let eventTimelineItem = timelineItem as? EventBasedTimelineItemProtocol else {
|
||||
@@ -994,7 +994,7 @@ class TimelineViewModel: TimelineViewModelType, TimelineViewModelProtocol {
|
||||
|
||||
state.bindings.readReceiptsSummaryInfo = .init(orderedReceipts: eventTimelineItem.properties.orderedReadReceipts, id: eventTimelineItem.id)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Message forwarding
|
||||
|
||||
private func forwardMessage(itemID: TimelineItemIdentifier) async {
|
||||
@@ -1120,9 +1120,9 @@ extension TimelineViewModel {
|
||||
clientProxyMock.roomSummaryForAliasReturnValue = .mock(id: "!room:matrix.org", name: "Room")
|
||||
clientProxyMock.roomSummaryForIdentifierReturnValue = .mock(id: "!room:matrix.org", name: "Room", canonicalAlias: "#room:matrix.org")
|
||||
let roomProxy = JoinedRoomProxyMock(.init(name: "Preview room", predecessor: hasPredecessor ? .init(roomId: UUID().uuidString) : nil))
|
||||
|
||||
|
||||
let appSettings = AppSettings.volatile()
|
||||
|
||||
|
||||
return TimelineViewModel(roomProxy: roomProxy,
|
||||
focussedEventID: nil,
|
||||
timelineController: timelineController ?? MockTimelineController(timelineKind: timelineKind),
|
||||
|
||||
@@ -36,14 +36,14 @@ struct TimelineItemMenu: View {
|
||||
if !actions.reactions.isEmpty {
|
||||
reactionsSection
|
||||
.padding(.bottom, 8.0)
|
||||
|
||||
|
||||
Divider()
|
||||
.background(Color.compound.bgSubtlePrimary)
|
||||
}
|
||||
|
||||
|
||||
if !actions.actions.isEmpty {
|
||||
viewsForActions(actions.actions)
|
||||
|
||||
|
||||
Divider()
|
||||
.background(Color.compound.bgSubtlePrimary)
|
||||
}
|
||||
@@ -278,7 +278,7 @@ struct TimelineItemMenu_Previews: PreviewProvider, TestablePreview {
|
||||
|
||||
static let (mediaItem, mediaItemActions) = makeActions(itemType: .outgoingMedia)
|
||||
static let (mediaItemWithCaption, mediaItemWithCaptionActions) = makeActions(itemType: .outgoingMediaWithCaption)
|
||||
|
||||
|
||||
static var previews: some View {
|
||||
TimelineItemMenu(item: item, actions: actions)
|
||||
.environmentObject(viewModel.context)
|
||||
|
||||
+7
-7
@@ -28,12 +28,12 @@ struct TimelineItemMenuActionProvider {
|
||||
// Don't show a context menu for non-event based items.
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
if timelineItem is StateRoomTimelineItem {
|
||||
// Don't show a context menu for state events.
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
if let encryptedItem = timelineItem as? EncryptedRoomTimelineItem {
|
||||
return makeEncryptedItemActions(encryptedItem)
|
||||
}
|
||||
@@ -48,7 +48,7 @@ struct TimelineItemMenuActionProvider {
|
||||
if canRedactItem(item), let poll = item.pollIfAvailable, !poll.hasEnded, let eventID = item.id.eventID {
|
||||
actions.append(.endPoll(pollStartID: eventID))
|
||||
}
|
||||
|
||||
|
||||
if item.canBeRepliedTo, canCurrentUserSendMessage {
|
||||
if let messageItem = item as? EventBasedMessageTimelineItemProtocol {
|
||||
// If threads are enabled we will have the dedicated `replyInThread` action
|
||||
@@ -88,7 +88,7 @@ struct TimelineItemMenuActionProvider {
|
||||
if canCurrentUserPin, let eventID = item.id.eventID {
|
||||
actions.append(pinnedEventIDs.contains(eventID) ? .unpin : .pin)
|
||||
}
|
||||
|
||||
|
||||
if item.isCopyable {
|
||||
actions.append(.copy)
|
||||
|
||||
@@ -141,17 +141,17 @@ struct TimelineItemMenuActionProvider {
|
||||
}
|
||||
|
||||
let isReactable = timelineKind == .live || timelineKind == .detached || timelineKind.isThread ? item.isReactable : false
|
||||
|
||||
|
||||
return .init(isReactable: isReactable, actions: actions, secondaryActions: secondaryActions, emojiProvider: emojiProvider)
|
||||
}
|
||||
|
||||
private func makeEncryptedItemActions(_ encryptedItem: EncryptedRoomTimelineItem) -> TimelineItemMenuActions? {
|
||||
var actions: [TimelineItemMenuAction] = [.copyPermalink]
|
||||
|
||||
|
||||
if isViewSourceEnabled {
|
||||
actions.append(.viewSource)
|
||||
}
|
||||
|
||||
|
||||
return .init(isReactable: false,
|
||||
actions: actions,
|
||||
secondaryActions: [],
|
||||
|
||||
@@ -15,14 +15,14 @@ struct PollOptionView: View {
|
||||
let pollOption: Poll.Option
|
||||
let showVotes: Bool
|
||||
let isFinalResult: Bool
|
||||
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 12) {
|
||||
Image(systemSymbol: pollOption.isSelected ? .checkmarkCircleFill : .circle)
|
||||
.font(.compound.bodyLG)
|
||||
.foregroundColor(pollOption.isSelected && isEnabled ? .compound.iconPrimary : .compound.iconTertiary)
|
||||
.accessibilityAddTraits(pollOption.isSelected ? .isSelected : [])
|
||||
|
||||
|
||||
VStack(spacing: 10) {
|
||||
HStack(alignment: .lastTextBaseline, spacing: 8) {
|
||||
Text(pollOption.text)
|
||||
@@ -30,7 +30,7 @@ struct PollOptionView: View {
|
||||
.multilineTextAlignment(.leading)
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
|
||||
if showVotes {
|
||||
if isFinalWinningOption {
|
||||
HStack(spacing: 4) {
|
||||
@@ -49,14 +49,14 @@ struct PollOptionView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
PollProgressView(progress: progress)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
|
||||
private var progress: Double {
|
||||
switch (showVotes, pollOption.allVotes, pollOption.isSelected) {
|
||||
case (true, let allVotes, _) where allVotes > 0:
|
||||
@@ -67,7 +67,7 @@ struct PollOptionView: View {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var isFinalWinningOption: Bool {
|
||||
pollOption.isWinning && isFinalResult
|
||||
}
|
||||
@@ -75,13 +75,13 @@ struct PollOptionView: View {
|
||||
|
||||
private struct PollProgressView: View {
|
||||
let progress: Double
|
||||
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geometry in
|
||||
ZStack(alignment: .leading) {
|
||||
Capsule()
|
||||
.foregroundColor(.compound._bgEmptyItemAlpha)
|
||||
|
||||
|
||||
Capsule()
|
||||
.frame(maxWidth: progress * geometry.size.width)
|
||||
}
|
||||
@@ -102,7 +102,7 @@ struct PollOptionView_Previews: PreviewProvider, TestablePreview {
|
||||
isWinning: false),
|
||||
showVotes: false,
|
||||
isFinalResult: false)
|
||||
|
||||
|
||||
PollOptionView(pollOption: .init(id: "2",
|
||||
text: "Chinese 🇨🇳",
|
||||
votes: 9,
|
||||
@@ -111,7 +111,7 @@ struct PollOptionView_Previews: PreviewProvider, TestablePreview {
|
||||
isWinning: true),
|
||||
showVotes: true,
|
||||
isFinalResult: false)
|
||||
|
||||
|
||||
PollOptionView(pollOption: .init(id: "2",
|
||||
text: "Chinese 🇨🇳",
|
||||
votes: 9,
|
||||
|
||||
@@ -59,7 +59,7 @@ struct PollView: View {
|
||||
.frame(maxWidth: 450)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private var senderString: String {
|
||||
@@ -76,14 +76,14 @@ struct PollView: View {
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var questionView: some View {
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
CompoundIcon(poll.hasEnded ? \.pollsEnd : \.polls,
|
||||
size: .custom(22),
|
||||
relativeTo: .compound.bodyLGSemibold)
|
||||
.accessibilityLabel(poll.hasEnded ? L10n.a11yPollEnd : L10n.a11yPoll)
|
||||
|
||||
|
||||
Text(poll.question)
|
||||
.multilineTextAlignment(.leading)
|
||||
.font(.compound.bodyLGSemibold)
|
||||
@@ -91,7 +91,7 @@ struct PollView: View {
|
||||
.accessibilityElement(children: .combine)
|
||||
.accessibilityAddTraits(.isHeader)
|
||||
}
|
||||
|
||||
|
||||
private var optionsView: some View {
|
||||
ForEach(poll.options, id: \.id) { option in
|
||||
pollOption(option: option)
|
||||
@@ -116,7 +116,7 @@ struct PollView: View {
|
||||
}
|
||||
.disabled(poll.hasEnded)
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private var summaryView: some View {
|
||||
if let summaryText = poll.summaryText {
|
||||
@@ -127,7 +127,7 @@ struct PollView: View {
|
||||
.frame(maxWidth: .infinity, alignment: showVotes ? .trailing : .leading)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private var toolbarView: some View {
|
||||
if !poll.hasEnded, poll.createdByAccountOwner {
|
||||
@@ -157,7 +157,7 @@ struct PollView: View {
|
||||
actionHandler(.end)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func progressBarColor(for option: Poll.Option) -> Color {
|
||||
if poll.hasEnded {
|
||||
return option.isWinning ? .compound.textActionAccent : .compound.textDisabled
|
||||
@@ -165,7 +165,7 @@ struct PollView: View {
|
||||
return .compound.textPrimary
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var showVotes: Bool {
|
||||
poll.hasEnded || poll.kind == .disclosed
|
||||
}
|
||||
@@ -178,7 +178,7 @@ private extension Poll {
|
||||
L10n.commonPollTotalVotes($0.allVotes)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
switch kind {
|
||||
case .disclosed:
|
||||
return options.first.map {
|
||||
@@ -195,19 +195,19 @@ struct PollView_Previews: PreviewProvider, TestablePreview {
|
||||
PollView(poll: .disclosed(), state: .full(isEditable: false), sender: .test) { _ in }
|
||||
.padding()
|
||||
.previewDisplayName("Disclosed")
|
||||
|
||||
|
||||
PollView(poll: .undisclosed(), state: .full(isEditable: false), sender: .test) { _ in }
|
||||
.padding()
|
||||
.previewDisplayName("Undisclosed")
|
||||
|
||||
|
||||
PollView(poll: .endedDisclosed, state: .full(isEditable: false), sender: .test) { _ in }
|
||||
.padding()
|
||||
.previewDisplayName("Ended, Disclosed")
|
||||
|
||||
|
||||
PollView(poll: .endedUndisclosed, state: .full(isEditable: false), sender: .test) { _ in }
|
||||
.padding()
|
||||
.previewDisplayName("Ended, Undisclosed")
|
||||
|
||||
|
||||
PollView(poll: .disclosed(createdByAccountOwner: true), state: .full(isEditable: true), sender: .test) { _ in }
|
||||
.padding()
|
||||
.previewDisplayName("Creator, disclosed")
|
||||
|
||||
@@ -23,7 +23,7 @@ struct ReadReceiptCell: View {
|
||||
}
|
||||
return readReceipt.userID
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 12) {
|
||||
LoadableAvatarImage(url: memberState?.avatarURL,
|
||||
|
||||
@@ -44,9 +44,9 @@ struct ReadReceiptsSummaryView_Previews: PreviewProvider, TestablePreview {
|
||||
.mockDan
|
||||
]
|
||||
let roomProxyMock = JoinedRoomProxyMock(.init(name: "Room", members: members))
|
||||
|
||||
|
||||
let appSettings = AppSettings.volatile()
|
||||
|
||||
|
||||
return TimelineViewModel(roomProxy: roomProxyMock,
|
||||
timelineController: MockTimelineController(),
|
||||
userSession: UserSessionMock(.init()),
|
||||
|
||||
@@ -144,7 +144,7 @@ private struct LongPressGestureRepresentable: UIGestureRecognizerRepresentable {
|
||||
func handleUIGestureRecognizerAction(_ recognizer: UILongPressGestureRecognizer, context: Context) {
|
||||
handle(recognizer)
|
||||
}
|
||||
|
||||
|
||||
class Coordinator: NSObject, UIGestureRecognizerDelegate {
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer,
|
||||
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
|
||||
@@ -69,7 +69,7 @@ struct TimelineBubbleLayout: Layout {
|
||||
|
||||
// Calculate the natural size using the regular text and non-greedy quote bubbles.
|
||||
let layoutSubviews = subviews.filter { $0[Size.self].shouldLayout }
|
||||
|
||||
|
||||
let subviewSizes = layoutSubviews.map { size(for: $0, subviews: subviews, proposedSize: proposal, cache: &cache) }
|
||||
|
||||
let maxWidth = subviewSizes.map(\.width).reduce(0, max)
|
||||
@@ -88,7 +88,7 @@ struct TimelineBubbleLayout: Layout {
|
||||
|
||||
// Place the regular text and greedy components using the calculated width.
|
||||
let visibleSubviews = subviews.filter { $0[Size.self].shouldRender }
|
||||
|
||||
|
||||
let subviewSizes = visibleSubviews.map { size(for: $0, subviews: subviews, proposedSize: ProposedViewSize(width: maxWidth, height: proposal.height), cache: &cache) }
|
||||
|
||||
var y = bounds.minY
|
||||
|
||||
@@ -17,15 +17,15 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
||||
let timelineItem: EventBasedTimelineItemProtocol
|
||||
let adjustedDeliveryStatus: TimelineItemDeliveryStatus?
|
||||
@ViewBuilder let content: () -> Content
|
||||
|
||||
|
||||
private var isDM: Bool {
|
||||
context.viewState.isDM
|
||||
}
|
||||
|
||||
|
||||
private var isFocussed: Bool {
|
||||
focussedEventID != nil && timelineItem.id.eventID == focussedEventID
|
||||
}
|
||||
|
||||
|
||||
private var isPinned: Bool {
|
||||
guard context.viewState.timelineKind != .pinned,
|
||||
let eventID = timelineItem.id.eventID else {
|
||||
@@ -51,17 +51,17 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
||||
header
|
||||
.zIndex(1)
|
||||
}
|
||||
|
||||
|
||||
VStack(alignment: alignment, spacing: 0) {
|
||||
HStack(spacing: 0) {
|
||||
if timelineItem.isOutgoing {
|
||||
Spacer()
|
||||
}
|
||||
|
||||
|
||||
messageBubbleWithReactions
|
||||
}
|
||||
.padding(timelineItem.isOutgoing ? .leading : .trailing, 48) // Additional padding to differentiate alignment.
|
||||
|
||||
|
||||
HStack(spacing: 0) {
|
||||
if !timelineItem.isOutgoing {
|
||||
Spacer()
|
||||
@@ -246,12 +246,12 @@ private extension EventBasedTimelineItemProtocol {
|
||||
return defaultColor
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// The insets for the full bubble content.
|
||||
/// Padding affecting just the "send info" should be added inside `TimelineItemSendInfoView`
|
||||
var bubbleInsets: EdgeInsets {
|
||||
let defaultInsets: EdgeInsets = .init(around: 8)
|
||||
|
||||
|
||||
switch self {
|
||||
case is StickerRoomTimelineItem:
|
||||
return .zero
|
||||
@@ -286,7 +286,7 @@ private extension EdgeInsets {
|
||||
init(around: CGFloat) {
|
||||
self.init(top: around, leading: around, bottom: around, trailing: around)
|
||||
}
|
||||
|
||||
|
||||
static var zero: Self = .init(around: 0)
|
||||
}
|
||||
|
||||
@@ -370,7 +370,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
|
||||
linkMetadataProvider: LinkMetadataProvider(),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
}()
|
||||
|
||||
|
||||
static var previews: some View {
|
||||
mockTimeline
|
||||
.previewDisplayName("Mock Timeline")
|
||||
@@ -465,7 +465,7 @@ struct TimelineItemBubbledStylerView_Previews: PreviewProvider, TestablePreview
|
||||
.environmentObject(viewModelWithPins.context)
|
||||
.environment(\.timelineContext, viewModel.context)
|
||||
}
|
||||
|
||||
|
||||
static var pinned: some View {
|
||||
ScrollView {
|
||||
MockTimelineContent(isPinned: true)
|
||||
@@ -646,7 +646,7 @@ private struct MockTimelineContent: View {
|
||||
isThreaded: isThreaded,
|
||||
threadSummary: threadSummary)),
|
||||
groupStyle: .single))
|
||||
|
||||
|
||||
AudioRoomTimelineView(timelineItem: .init(id: makeItemIdentifier(),
|
||||
timestamp: .mock,
|
||||
isOutgoing: true,
|
||||
|
||||
@@ -14,7 +14,7 @@ enum TimelineGroupStyle: Hashable {
|
||||
case first
|
||||
case middle
|
||||
case last
|
||||
|
||||
|
||||
var shouldShowSenderDetails: Bool {
|
||||
switch self {
|
||||
case .single, .first:
|
||||
|
||||
@@ -23,7 +23,7 @@ struct TimelineStyler<Content: View>: View {
|
||||
self.content = content
|
||||
_adjustedDeliveryStatus = State(initialValue: timelineItem.properties.deliveryStatus)
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
mainContent
|
||||
.onChange(of: timelineItem.properties.deliveryStatus) { _, newStatus in
|
||||
@@ -56,7 +56,7 @@ struct TimelineStyler<Content: View>: View {
|
||||
|
||||
struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = TimelineViewModel.mock
|
||||
|
||||
|
||||
static let base = TextRoomTimelineItem(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isOutgoing: true,
|
||||
@@ -64,19 +64,19 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
|
||||
canBeRepliedTo: true,
|
||||
sender: .test,
|
||||
content: .init(body: "Test"))
|
||||
|
||||
|
||||
static let sentNonLast: TextRoomTimelineItem = {
|
||||
var result = base
|
||||
result.properties.deliveryStatus = .sent
|
||||
return result
|
||||
}()
|
||||
|
||||
|
||||
static let sendingNonLast: TextRoomTimelineItem = {
|
||||
var result = base
|
||||
result.properties.deliveryStatus = .sending
|
||||
return result
|
||||
}()
|
||||
|
||||
|
||||
static let sendingLast: TextRoomTimelineItem = {
|
||||
let id = viewModel.state.timelineState.uniqueIDs.last ?? .init(UUID().uuidString)
|
||||
var result = TextRoomTimelineItem(id: .event(uniqueID: id, eventOrTransactionID: .eventID(UUID().uuidString)),
|
||||
@@ -89,13 +89,13 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
|
||||
result.properties.deliveryStatus = .sending
|
||||
return result
|
||||
}()
|
||||
|
||||
|
||||
static let failed: TextRoomTimelineItem = {
|
||||
var result = base
|
||||
result.properties.deliveryStatus = .sendingFailed(.unknown)
|
||||
return result
|
||||
}()
|
||||
|
||||
|
||||
static let sentLast: TextRoomTimelineItem = {
|
||||
let id = viewModel.state.timelineState.uniqueIDs.last ?? .init(UUID().uuidString)
|
||||
return TextRoomTimelineItem(id: .event(uniqueID: id, eventOrTransactionID: .eventID(UUID().uuidString)),
|
||||
@@ -106,21 +106,21 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
|
||||
sender: .test,
|
||||
content: .init(body: "Test"))
|
||||
}()
|
||||
|
||||
|
||||
static let ltrString = TextRoomTimelineItem(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isOutgoing: true,
|
||||
isEditable: false,
|
||||
canBeRepliedTo: true,
|
||||
sender: .test, content: .init(body: "house!"))
|
||||
|
||||
|
||||
static let rtlString = TextRoomTimelineItem(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isOutgoing: true,
|
||||
isEditable: false,
|
||||
canBeRepliedTo: true,
|
||||
sender: .test, content: .init(body: "באמת!"))
|
||||
|
||||
|
||||
static let ltrStringThatContainsRtl = TextRoomTimelineItem(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isOutgoing: true,
|
||||
@@ -128,7 +128,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
|
||||
canBeRepliedTo: true,
|
||||
sender: .test,
|
||||
content: .init(body: "house! -- באמת! -- house!"))
|
||||
|
||||
|
||||
static let rtlStringThatContainsLtr = TextRoomTimelineItem(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isOutgoing: true,
|
||||
@@ -136,7 +136,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
|
||||
canBeRepliedTo: true,
|
||||
sender: .test,
|
||||
content: .init(body: "באמת! -- house! -- באמת!"))
|
||||
|
||||
|
||||
static let ltrStringThatFinishesInRtl = TextRoomTimelineItem(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isOutgoing: true,
|
||||
@@ -144,7 +144,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
|
||||
canBeRepliedTo: true,
|
||||
sender: .test,
|
||||
content: .init(body: "house! -- באמת!"))
|
||||
|
||||
|
||||
static let rtlStringThatFinishesInLtr = TextRoomTimelineItem(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isOutgoing: true,
|
||||
@@ -161,7 +161,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
|
||||
shouldBoost: true,
|
||||
sender: .test,
|
||||
content: .init(body: "😮"))
|
||||
|
||||
|
||||
static let endingWithBlockquote: TextRoomTimelineItem = {
|
||||
let builder = AttributedStringBuilder(cacheKey: "preview", mentionBuilder: MentionBuilder())
|
||||
let attributedString = builder.fromHTML("<p>Some text before</p><blockquote>A quoted line at the end</blockquote>")
|
||||
@@ -173,7 +173,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
|
||||
sender: .test,
|
||||
content: .init(body: "", formattedBody: attributedString))
|
||||
}()
|
||||
|
||||
|
||||
static let endingWithCodeblock: TextRoomTimelineItem = {
|
||||
let builder = AttributedStringBuilder(cacheKey: "preview", mentionBuilder: MentionBuilder())
|
||||
let attributedString = builder.fromHTML("<p>Some text before</p><pre><code>let x = 42</code></pre>")
|
||||
@@ -185,7 +185,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
|
||||
sender: .test,
|
||||
content: .init(body: "", formattedBody: attributedString))
|
||||
}()
|
||||
|
||||
|
||||
static var testView: some View {
|
||||
VStack(spacing: 0) {
|
||||
TextRoomTimelineView(timelineItem: base)
|
||||
@@ -198,7 +198,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
|
||||
TextRoomTimelineView(timelineItem: endingWithCodeblock)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static var languagesTestView: some View {
|
||||
VStack(spacing: 0) {
|
||||
TextRoomTimelineView(timelineItem: ltrString)
|
||||
@@ -210,7 +210,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
|
||||
TextRoomTimelineView(timelineItem: bigEmoji)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static var previews: some View {
|
||||
testView
|
||||
.environmentObject(viewModel.context)
|
||||
@@ -219,7 +219,7 @@ struct TimelineItemStyler_Previews: PreviewProvider, TestablePreview {
|
||||
languagesTestView
|
||||
.environmentObject(viewModel.context)
|
||||
.previewDisplayName("Bubbles LTR with different layout languages")
|
||||
|
||||
|
||||
languagesTestView
|
||||
.environmentObject(viewModel.context)
|
||||
.environment(\.layoutDirection, .rightToLeft)
|
||||
|
||||
+2
-2
@@ -14,9 +14,9 @@ struct TimelineDeliveryStatusView: View {
|
||||
case sending
|
||||
case sent
|
||||
}
|
||||
|
||||
|
||||
let deliveryStatus: Status
|
||||
|
||||
|
||||
private var icon: CompoundIcon {
|
||||
switch deliveryStatus {
|
||||
case .sending:
|
||||
|
||||
@@ -13,15 +13,15 @@ struct TimelineItemStatusView: View {
|
||||
let timelineItem: EventBasedTimelineItemProtocol
|
||||
let adjustedDeliveryStatus: TimelineItemDeliveryStatus?
|
||||
@EnvironmentObject private var context: TimelineViewModel.Context
|
||||
|
||||
|
||||
private var isLastOutgoingMessage: Bool {
|
||||
timelineItem.isOutgoing && context.viewState.timelineState.uniqueIDs.last == timelineItem.id.uniqueID
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
mainContent
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private var mainContent: some View {
|
||||
if context.viewState.timelineKind == .pinned {
|
||||
@@ -33,7 +33,7 @@ struct TimelineItemStatusView: View {
|
||||
deliveryStatusBadge
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
var deliveryStatusBadge: some View {
|
||||
switch adjustedDeliveryStatus {
|
||||
@@ -49,7 +49,7 @@ struct TimelineItemStatusView: View {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var readReceipts: some View {
|
||||
TimelineReadReceiptsView(timelineItem: timelineItem)
|
||||
.environmentObject(context)
|
||||
|
||||
@@ -13,7 +13,7 @@ import SwiftUI
|
||||
struct TimelineReactionsView: View {
|
||||
private let feedbackGenerator = UIImpactFeedbackGenerator(style: .heavy)
|
||||
@Environment(\.layoutDirection) private var layoutDirection: LayoutDirection
|
||||
|
||||
|
||||
let context: TimelineViewModel.Context
|
||||
let itemID: TimelineItemIdentifier
|
||||
let reactions: [AggregatedReaction]
|
||||
|
||||
@@ -20,7 +20,7 @@ struct TimelineReadReceiptsView: View {
|
||||
contentID: receipt.userID)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 2) {
|
||||
StackedAvatarsView(overlap: 6,
|
||||
@@ -78,10 +78,10 @@ struct TimelineReadReceiptsView_Previews: PreviewProvider, TestablePreview {
|
||||
.mockDan,
|
||||
.mockMe
|
||||
]
|
||||
|
||||
|
||||
static let viewModel = {
|
||||
let appSettings = AppSettings.volatile()
|
||||
|
||||
|
||||
return TimelineViewModel(roomProxy: JoinedRoomProxyMock(.init(name: "Test", members: members)),
|
||||
timelineController: MockTimelineController(),
|
||||
userSession: UserSessionMock(.init()),
|
||||
@@ -94,7 +94,7 @@ struct TimelineReadReceiptsView_Previews: PreviewProvider, TestablePreview {
|
||||
linkMetadataProvider: LinkMetadataProvider(),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
}()
|
||||
|
||||
|
||||
static let singleReceipt = [ReadReceipt(userID: RoomMemberProxyMock.mockAlice.userID, formattedTimestamp: "Now")]
|
||||
static let doubleReceipt = [ReadReceipt(userID: RoomMemberProxyMock.mockAlice.userID, formattedTimestamp: "Now"),
|
||||
ReadReceipt(userID: RoomMemberProxyMock.mockBob.userID, formattedTimestamp: "Before")]
|
||||
@@ -105,7 +105,7 @@ struct TimelineReadReceiptsView_Previews: PreviewProvider, TestablePreview {
|
||||
ReadReceipt(userID: RoomMemberProxyMock.mockBob.userID, formattedTimestamp: "Before"),
|
||||
ReadReceipt(userID: RoomMemberProxyMock.mockCharlie.userID, formattedTimestamp: "Way before"),
|
||||
ReadReceipt(userID: RoomMemberProxyMock.mockDan.userID, formattedTimestamp: "Way, way before")]
|
||||
|
||||
|
||||
static func mockTimelineItem(with receipts: [ReadReceipt]) -> TextRoomTimelineItem {
|
||||
TextRoomTimelineItem(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
@@ -115,7 +115,7 @@ struct TimelineReadReceiptsView_Previews: PreviewProvider, TestablePreview {
|
||||
sender: .init(id: UUID().uuidString), content: .init(body: "Test"),
|
||||
properties: .init(orderedReadReceipts: receipts))
|
||||
}
|
||||
|
||||
|
||||
static var previews: some View {
|
||||
VStack(spacing: 8) {
|
||||
TimelineReadReceiptsView(timelineItem: mockTimelineItem(with: singleReceipt))
|
||||
|
||||
+18
-18
@@ -20,14 +20,14 @@ struct CallNotificationRoomTimelineView: View {
|
||||
CompoundIcon(iconKeyPath, size: .medium, relativeTo: .compound.headingMDBold)
|
||||
.foregroundStyle(.compound.iconSecondary)
|
||||
.accessibilityHidden(true)
|
||||
|
||||
|
||||
Text(tileTitle)
|
||||
.font(.compound.bodyMD)
|
||||
.foregroundColor(.compound.textSecondary)
|
||||
.labelStyle(.custom(spacing: 4))
|
||||
|
||||
|
||||
Spacer()
|
||||
|
||||
|
||||
Text(timelineItem.timestamp.formattedTime())
|
||||
.font(.compound.bodyXS)
|
||||
.foregroundColor(.compound.textSecondary)
|
||||
@@ -39,7 +39,7 @@ struct CallNotificationRoomTimelineView: View {
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
|
||||
private var tileTitle: String {
|
||||
if timelineItem.isDM {
|
||||
// As per design only have declined variants in DM
|
||||
@@ -54,7 +54,7 @@ struct CallNotificationRoomTimelineView: View {
|
||||
L10n.commonCallStarted
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var iconKeyPath: KeyPath<CompoundIcons, Image> {
|
||||
if timelineItem.isDM, timelineItem.isDeclined || timelineItem.isDeclinedByMe {
|
||||
// As per design only have declined variants in DM
|
||||
@@ -82,9 +82,9 @@ struct CallNotificationRoomTimelineView_Previews: PreviewProvider, TestablePrevi
|
||||
isDeclinedByMe: false,
|
||||
isDeclined: false,
|
||||
isVoiceCall: false))
|
||||
|
||||
|
||||
Divider()
|
||||
|
||||
|
||||
CallNotificationRoomTimelineView(timelineItem: .init(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isEditable: false,
|
||||
@@ -93,9 +93,9 @@ struct CallNotificationRoomTimelineView_Previews: PreviewProvider, TestablePrevi
|
||||
isDeclinedByMe: false,
|
||||
isDeclined: false,
|
||||
isVoiceCall: true))
|
||||
|
||||
|
||||
Divider()
|
||||
|
||||
|
||||
CallNotificationRoomTimelineView(timelineItem: .init(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isEditable: false,
|
||||
@@ -104,9 +104,9 @@ struct CallNotificationRoomTimelineView_Previews: PreviewProvider, TestablePrevi
|
||||
isDeclinedByMe: true,
|
||||
isDeclined: true,
|
||||
isVoiceCall: false))
|
||||
|
||||
|
||||
Divider()
|
||||
|
||||
|
||||
CallNotificationRoomTimelineView(timelineItem: .init(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isEditable: false,
|
||||
@@ -115,9 +115,9 @@ struct CallNotificationRoomTimelineView_Previews: PreviewProvider, TestablePrevi
|
||||
isDeclinedByMe: false,
|
||||
isDeclined: true,
|
||||
isVoiceCall: true))
|
||||
|
||||
|
||||
Divider()
|
||||
|
||||
|
||||
CallNotificationRoomTimelineView(timelineItem: .init(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isEditable: false,
|
||||
@@ -126,9 +126,9 @@ struct CallNotificationRoomTimelineView_Previews: PreviewProvider, TestablePrevi
|
||||
isDeclinedByMe: true,
|
||||
isDeclined: true,
|
||||
isVoiceCall: true))
|
||||
|
||||
|
||||
Divider()
|
||||
|
||||
|
||||
CallNotificationRoomTimelineView(timelineItem: .init(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isEditable: false,
|
||||
@@ -137,9 +137,9 @@ struct CallNotificationRoomTimelineView_Previews: PreviewProvider, TestablePrevi
|
||||
isDeclinedByMe: false,
|
||||
isDeclined: true,
|
||||
isVoiceCall: false))
|
||||
|
||||
|
||||
Divider()
|
||||
|
||||
|
||||
CallNotificationRoomTimelineView(timelineItem: .init(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isEditable: false,
|
||||
@@ -148,7 +148,7 @@ struct CallNotificationRoomTimelineView_Previews: PreviewProvider, TestablePrevi
|
||||
isDeclinedByMe: true,
|
||||
isDeclined: true,
|
||||
isVoiceCall: false))
|
||||
|
||||
|
||||
Divider()
|
||||
}
|
||||
.padding()
|
||||
|
||||
@@ -73,7 +73,7 @@ struct FormattedBodyText: View {
|
||||
type: .plainText))
|
||||
}
|
||||
let lastPlainTextIndex = components.lastIndex { $0.type == .plainText }
|
||||
|
||||
|
||||
return TimelineBubbleLayout(spacing: 8) {
|
||||
// The `ForEach` needs to iterate over the id of the element to allow
|
||||
// SwiftUI animations to work properly ater any edit.
|
||||
@@ -98,7 +98,7 @@ struct FormattedBodyText: View {
|
||||
.timelineBubbleLayoutSize(.natural)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Make a second iteration through the components adding naturally sized versions of the
|
||||
// block quotes and code blocks which are used for layout calculations but won't be rendered.
|
||||
ForEach(components) { component in
|
||||
|
||||
+2
-2
@@ -91,7 +91,7 @@ struct HighlightedTimelineItemTimeline_Previews: PreviewProvider {
|
||||
static let composerViewModel = ComposerToolbarViewModel.mock()
|
||||
static let timelineViewModel = {
|
||||
let appSettings = AppSettings.volatile()
|
||||
|
||||
|
||||
return TimelineViewModel(roomProxy: roomProxyMock,
|
||||
focussedEventID: focussedEventID,
|
||||
timelineController: MockTimelineController(),
|
||||
@@ -105,7 +105,7 @@ struct HighlightedTimelineItemTimeline_Previews: PreviewProvider {
|
||||
linkMetadataProvider: LinkMetadataProvider(),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
}()
|
||||
|
||||
|
||||
static var previews: some View {
|
||||
ElementNavigationStack {
|
||||
RoomScreen(context: roomViewModel.context,
|
||||
|
||||
+1
-1
@@ -65,7 +65,7 @@ struct ImageRoomTimelineView: View {
|
||||
.timelineMediaFrame(imageInfo: timelineItem.content.thumbnailInfo ?? timelineItem.content.imageInfo)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var placeholder: some View {
|
||||
Rectangle()
|
||||
.foregroundColor(timelineItem.isOutgoing ? .compound._bgBubbleOutgoing : .compound._bgBubbleIncoming)
|
||||
|
||||
+6
-6
@@ -23,7 +23,7 @@ struct LocationRoomTimelineView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private var mainContent: some View {
|
||||
if let geoURI = timelineItem.content.geoURI {
|
||||
@@ -41,16 +41,16 @@ struct LocationRoomTimelineView: View {
|
||||
trailingReservedSize: timelineItem.trailingReservedSize)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
|
||||
private let mapAspectRatio: Double = 3 / 2
|
||||
private let mapMaxHeight: Double = 300
|
||||
}
|
||||
|
||||
struct LocationRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = TimelineViewModel.mock
|
||||
|
||||
|
||||
static var previews: some View {
|
||||
PreviewScrollView {
|
||||
VStack(spacing: 8) {
|
||||
@@ -62,7 +62,7 @@ struct LocationRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
||||
.previewLayout(.sizeThatFits)
|
||||
.previewDisplayName("Bubbles")
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
static var states: some View {
|
||||
LocationRoomTimelineView(timelineItem: .init(id: .randomEvent,
|
||||
@@ -72,7 +72,7 @@ struct LocationRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
||||
canBeRepliedTo: true,
|
||||
sender: .init(id: "Bob"),
|
||||
content: .init(body: "Fallback geo uri description")))
|
||||
|
||||
|
||||
LocationRoomTimelineView(timelineItem: .init(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
isOutgoing: false,
|
||||
|
||||
@@ -54,24 +54,24 @@ struct PollRoomTimelineView: View {
|
||||
struct PollRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = TimelineViewModel.mock
|
||||
static let pinnedEventsTimelineViewModel = TimelineViewModel.mock(timelineKind: .pinned)
|
||||
|
||||
|
||||
static var previews: some View {
|
||||
PollRoomTimelineView(timelineItem: .mock(poll: .disclosed(), isOutgoing: false))
|
||||
.environmentObject(viewModel.context)
|
||||
.previewDisplayName("Disclosed, Bubble")
|
||||
|
||||
|
||||
PollRoomTimelineView(timelineItem: .mock(poll: .undisclosed(), isOutgoing: false))
|
||||
.environmentObject(viewModel.context)
|
||||
.previewDisplayName("Undisclosed, Bubble")
|
||||
|
||||
|
||||
PollRoomTimelineView(timelineItem: .mock(poll: .endedDisclosed))
|
||||
.environmentObject(viewModel.context)
|
||||
.previewDisplayName("Ended, Disclosed, Bubble")
|
||||
|
||||
|
||||
PollRoomTimelineView(timelineItem: .mock(poll: .endedUndisclosed))
|
||||
.environmentObject(viewModel.context)
|
||||
.previewDisplayName("Ended, Undisclosed, Bubble")
|
||||
|
||||
|
||||
PollRoomTimelineView(timelineItem: .mock(poll: .disclosed(createdByAccountOwner: true)))
|
||||
.environmentObject(viewModel.context)
|
||||
.previewDisplayName("Creator, disclosed, Bubble")
|
||||
|
||||
+4
-4
@@ -29,9 +29,9 @@ struct ReadMarkerRoomTimelineView: View {
|
||||
|
||||
struct ReadMarkerRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
||||
static let viewModel = TimelineViewModel.mock
|
||||
|
||||
|
||||
static let item = ReadMarkerRoomTimelineItem(id: .randomVirtual)
|
||||
|
||||
|
||||
static var previews: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
RoomTimelineItemView(viewState: .init(type: .separator(.init(id: .virtual(uniqueID: .init("Separator")), timestamp: .mock)), groupStyle: .single))
|
||||
@@ -42,9 +42,9 @@ struct ReadMarkerRoomTimelineView_Previews: PreviewProvider, TestablePreview {
|
||||
canBeRepliedTo: true,
|
||||
sender: .init(id: "1", displayName: "Bob"),
|
||||
content: .init(body: "This is another message"))), groupStyle: .single))
|
||||
|
||||
|
||||
ReadMarkerRoomTimelineView(timelineItem: item)
|
||||
|
||||
|
||||
RoomTimelineItemView(viewState: .init(type: .separator(.init(id: .virtual(uniqueID: .init("Separator")), timestamp: .mock)), groupStyle: .single))
|
||||
RoomTimelineItemView(viewState: .init(type: .text(.init(id: .randomEvent,
|
||||
timestamp: .mock,
|
||||
|
||||
+1
-1
@@ -9,6 +9,6 @@
|
||||
// periphery: ignore - markdown protocol
|
||||
protocol TextBasedRoomTimelineViewProtocol {
|
||||
associatedtype TimelineItemType: TextBasedRoomTimelineItem
|
||||
|
||||
|
||||
var timelineItem: TimelineItemType { get }
|
||||
}
|
||||
|
||||
+2
-2
@@ -37,11 +37,11 @@ struct VoiceMessageRoomTimelineContent: View {
|
||||
onPlaybackSpeedChange: onPlaybackSpeedChange)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
|
||||
|
||||
private func onPlaybackPlayPause() {
|
||||
context?.send(viewAction: .handleAudioPlayerAction(.playPause(itemID: timelineItem.id)))
|
||||
}
|
||||
|
||||
|
||||
private func onPlaybackSpeedChange() {
|
||||
context?.send(viewAction: .handleAudioPlayerAction(.changePlaybackSpeed(itemID: timelineItem.id)))
|
||||
}
|
||||
|
||||
@@ -11,9 +11,9 @@ import SwiftUI
|
||||
|
||||
struct TimelineSenderAvatarView: View {
|
||||
@Environment(\.timelineContext) private var context
|
||||
|
||||
|
||||
let timelineItem: EventBasedTimelineItemProtocol
|
||||
|
||||
|
||||
var body: some View {
|
||||
LoadableAvatarImage(url: timelineItem.sender.avatarURL,
|
||||
name: timelineItem.sender.displayName,
|
||||
|
||||
@@ -79,7 +79,7 @@ struct TimelineView: View {
|
||||
/// A table view wrapper that displays the timeline of a room.
|
||||
struct TimelineViewRepresentable: UIViewControllerRepresentable {
|
||||
@EnvironmentObject private var viewModelContext: TimelineViewModel.Context
|
||||
|
||||
|
||||
func makeUIViewController(context: Context) -> TimelineTableViewController {
|
||||
TimelineTableViewController(coordinator: context.coordinator,
|
||||
isScrolledToBottom: $viewModelContext.isScrolledToBottom,
|
||||
@@ -133,7 +133,7 @@ struct TimelineViewRepresentable: UIViewControllerRepresentable {
|
||||
if tableViewController.readMarkerUniqueID != context.viewState.timelineState.readMarkerUniqueID {
|
||||
tableViewController.readMarkerUniqueID = context.viewState.timelineState.readMarkerUniqueID
|
||||
}
|
||||
|
||||
|
||||
if tableViewController.typingMembers.members != context.viewState.typingMembers {
|
||||
tableViewController.setTypingMembers(context.viewState.typingMembers)
|
||||
}
|
||||
@@ -154,7 +154,7 @@ struct TimelineView_Previews: PreviewProvider { // Not testable as this preview
|
||||
static let composerViewModel = ComposerToolbarViewModel.mock()
|
||||
static let timelineViewModel = {
|
||||
let appSettings = AppSettings.volatile()
|
||||
|
||||
|
||||
return TimelineViewModel(roomProxy: roomProxyMock,
|
||||
timelineController: MockTimelineController(),
|
||||
userSession: UserSessionMock(.init()),
|
||||
@@ -167,7 +167,7 @@ struct TimelineView_Previews: PreviewProvider { // Not testable as this preview
|
||||
linkMetadataProvider: LinkMetadataProvider(),
|
||||
timelineControllerFactory: TimelineControllerFactoryMock(.init()))
|
||||
}()
|
||||
|
||||
|
||||
static var previews: some View {
|
||||
ElementNavigationStack {
|
||||
RoomScreen(context: roomViewModel.context,
|
||||
|
||||
@@ -15,12 +15,12 @@ struct VoiceMessageRoomPlaybackView: View {
|
||||
@ScaledMetric private var waveformLineWidth = 2.0
|
||||
@ScaledMetric private var waveformLinePadding = 2.0
|
||||
@GestureState var isDragging = false
|
||||
|
||||
|
||||
let onPlayPause: () -> Void
|
||||
let onSeek: (Double) -> Void
|
||||
let onScrubbing: (Bool) -> Void
|
||||
let onPlaybackSpeedChange: () -> Void
|
||||
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 8) {
|
||||
VoiceMessageButton(state: .init(playerState.playerButtonPlaybackState),
|
||||
@@ -36,7 +36,7 @@ struct VoiceMessageRoomPlaybackView: View {
|
||||
.monospacedDigit()
|
||||
.fixedSize(horizontal: true, vertical: true)
|
||||
}
|
||||
|
||||
|
||||
waveformView
|
||||
.waveformInteraction(isDragging: $isDragging,
|
||||
progress: playerState.progress,
|
||||
@@ -62,7 +62,7 @@ struct VoiceMessageRoomPlaybackView: View {
|
||||
DateFormatter.elapsedTimeFormatter.string(from: duration)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var timeLabelContent: String {
|
||||
// Display the duration if progress is 0.0
|
||||
let percent = playerState.progress > 0.0 ? playerState.progress : 1.0
|
||||
@@ -82,7 +82,7 @@ struct VoiceMessageRoomPlaybackView: View {
|
||||
L10n.a11yVoiceMessage(durationString)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private var waveformView: some View {
|
||||
if let url = playerState.fileURL {
|
||||
@@ -96,7 +96,7 @@ struct VoiceMessageRoomPlaybackView: View {
|
||||
estimatedWaveformView
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var estimatedWaveformView: some View {
|
||||
EstimatedWaveformView(lineWidth: waveformLineWidth,
|
||||
linePadding: waveformLinePadding,
|
||||
@@ -111,7 +111,7 @@ private extension DateFormatter {
|
||||
dateFormatter.dateFormat = "m:ss"
|
||||
return dateFormatter
|
||||
}()
|
||||
|
||||
|
||||
static let longElapsedTimeFormatter: DateFormatter = {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "mm:ss"
|
||||
|
||||
Reference in New Issue
Block a user