Run SwiftFormat.

This commit is contained in:
Doug
2026-05-27 14:38:50 +01:00
committed by Doug
parent 5138e05e47
commit fa67713726
573 changed files with 2426 additions and 2426 deletions
@@ -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)
@@ -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)
@@ -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))
@@ -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
@@ -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,
@@ -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)
@@ -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")
@@ -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,
@@ -9,6 +9,6 @@
// periphery: ignore - markdown protocol
protocol TextBasedRoomTimelineViewProtocol {
associatedtype TimelineItemType: TextBasedRoomTimelineItem
var timelineItem: TimelineItemType { get }
}
@@ -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"