mirror of
https://github.com/element-hq/element-x-ios.git
synced 2025-03-10 21:39:12 +00:00
Pinned items timeline implementation for the banner (#3099)
This commit is contained in:
parent
a11faeb131
commit
ff2c42d53b
@ -8320,7 +8320,7 @@ class RoomProxyMock: RoomProxyProtocol {
|
|||||||
return pinnedEventIDsCallsCount > 0
|
return pinnedEventIDsCallsCount > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
var pinnedEventIDs: [String] {
|
var pinnedEventIDs: Set<String> {
|
||||||
get async {
|
get async {
|
||||||
pinnedEventIDsCallsCount += 1
|
pinnedEventIDsCallsCount += 1
|
||||||
if let pinnedEventIDsClosure = pinnedEventIDsClosure {
|
if let pinnedEventIDsClosure = pinnedEventIDsClosure {
|
||||||
@ -8330,8 +8330,8 @@ class RoomProxyMock: RoomProxyProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var underlyingPinnedEventIDs: [String]!
|
var underlyingPinnedEventIDs: Set<String>!
|
||||||
var pinnedEventIDsClosure: (() async -> [String])?
|
var pinnedEventIDsClosure: (() async -> Set<String>)?
|
||||||
var membership: Membership {
|
var membership: Membership {
|
||||||
get { return underlyingMembership }
|
get { return underlyingMembership }
|
||||||
set(value) { underlyingMembership = value }
|
set(value) { underlyingMembership = value }
|
||||||
@ -8403,6 +8403,23 @@ class RoomProxyMock: RoomProxyProtocol {
|
|||||||
set(value) { underlyingTimeline = value }
|
set(value) { underlyingTimeline = value }
|
||||||
}
|
}
|
||||||
var underlyingTimeline: TimelineProxyProtocol!
|
var underlyingTimeline: TimelineProxyProtocol!
|
||||||
|
var pinnedEventsTimelineCallsCount = 0
|
||||||
|
var pinnedEventsTimelineCalled: Bool {
|
||||||
|
return pinnedEventsTimelineCallsCount > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
var pinnedEventsTimeline: TimelineProxyProtocol? {
|
||||||
|
get async {
|
||||||
|
pinnedEventsTimelineCallsCount += 1
|
||||||
|
if let pinnedEventsTimelineClosure = pinnedEventsTimelineClosure {
|
||||||
|
return await pinnedEventsTimelineClosure()
|
||||||
|
} else {
|
||||||
|
return underlyingPinnedEventsTimeline
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var underlyingPinnedEventsTimeline: TimelineProxyProtocol?
|
||||||
|
var pinnedEventsTimelineClosure: (() async -> TimelineProxyProtocol?)?
|
||||||
|
|
||||||
//MARK: - subscribeForUpdates
|
//MARK: - subscribeForUpdates
|
||||||
|
|
||||||
|
@ -10989,6 +10989,42 @@ open class RoomSDKMock: MatrixRustSDK.Room {
|
|||||||
try await clearComposerDraftClosure?()
|
try await clearComposerDraftClosure?()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//MARK: - clearPinnedEventsCache
|
||||||
|
|
||||||
|
var clearPinnedEventsCacheUnderlyingCallsCount = 0
|
||||||
|
open var clearPinnedEventsCacheCallsCount: Int {
|
||||||
|
get {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
return clearPinnedEventsCacheUnderlyingCallsCount
|
||||||
|
} else {
|
||||||
|
var returnValue: Int? = nil
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
returnValue = clearPinnedEventsCacheUnderlyingCallsCount
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
clearPinnedEventsCacheUnderlyingCallsCount = newValue
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
clearPinnedEventsCacheUnderlyingCallsCount = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
open var clearPinnedEventsCacheCalled: Bool {
|
||||||
|
return clearPinnedEventsCacheCallsCount > 0
|
||||||
|
}
|
||||||
|
open var clearPinnedEventsCacheClosure: (() async -> Void)?
|
||||||
|
|
||||||
|
open override func clearPinnedEventsCache() async {
|
||||||
|
clearPinnedEventsCacheCallsCount += 1
|
||||||
|
await clearPinnedEventsCacheClosure?()
|
||||||
|
}
|
||||||
|
|
||||||
//MARK: - discardRoomKey
|
//MARK: - discardRoomKey
|
||||||
|
|
||||||
open var discardRoomKeyThrowableError: Error?
|
open var discardRoomKeyThrowableError: Error?
|
||||||
@ -13005,6 +13041,81 @@ open class RoomSDKMock: MatrixRustSDK.Room {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//MARK: - pinnedEventsTimeline
|
||||||
|
|
||||||
|
open var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadThrowableError: Error?
|
||||||
|
var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadUnderlyingCallsCount = 0
|
||||||
|
open var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadCallsCount: Int {
|
||||||
|
get {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
return pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadUnderlyingCallsCount
|
||||||
|
} else {
|
||||||
|
var returnValue: Int? = nil
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
returnValue = pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadUnderlyingCallsCount
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadUnderlyingCallsCount = newValue
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadUnderlyingCallsCount = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
open var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadCalled: Bool {
|
||||||
|
return pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadCallsCount > 0
|
||||||
|
}
|
||||||
|
open var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadReceivedArguments: (internalIdPrefix: String?, maxEventsToLoad: UInt16)?
|
||||||
|
open var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadReceivedInvocations: [(internalIdPrefix: String?, maxEventsToLoad: UInt16)] = []
|
||||||
|
|
||||||
|
var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadUnderlyingReturnValue: Timeline!
|
||||||
|
open var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadReturnValue: Timeline! {
|
||||||
|
get {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
return pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadUnderlyingReturnValue
|
||||||
|
} else {
|
||||||
|
var returnValue: Timeline? = nil
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
returnValue = pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadUnderlyingReturnValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if Thread.isMainThread {
|
||||||
|
pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadUnderlyingReturnValue = newValue
|
||||||
|
} else {
|
||||||
|
DispatchQueue.main.sync {
|
||||||
|
pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadUnderlyingReturnValue = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
open var pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadClosure: ((String?, UInt16) async throws -> Timeline)?
|
||||||
|
|
||||||
|
open override func pinnedEventsTimeline(internalIdPrefix: String?, maxEventsToLoad: UInt16) async throws -> Timeline {
|
||||||
|
if let error = pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadThrowableError {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadCallsCount += 1
|
||||||
|
pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadReceivedArguments = (internalIdPrefix: internalIdPrefix, maxEventsToLoad: maxEventsToLoad)
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadReceivedInvocations.append((internalIdPrefix: internalIdPrefix, maxEventsToLoad: maxEventsToLoad))
|
||||||
|
}
|
||||||
|
if let pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadClosure = pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadClosure {
|
||||||
|
return try await pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadClosure(internalIdPrefix, maxEventsToLoad)
|
||||||
|
} else {
|
||||||
|
return pinnedEventsTimelineInternalIdPrefixMaxEventsToLoadReturnValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
//MARK: - rawName
|
//MARK: - rawName
|
||||||
|
|
||||||
var rawNameUnderlyingCallsCount = 0
|
var rawNameUnderlyingCallsCount = 0
|
||||||
|
@ -29,7 +29,7 @@ struct RoomProxyMockConfiguration {
|
|||||||
var isEncrypted = true
|
var isEncrypted = true
|
||||||
var hasOngoingCall = true
|
var hasOngoingCall = true
|
||||||
var canonicalAlias: String?
|
var canonicalAlias: String?
|
||||||
var pinnedEventIDs: [String] = []
|
var pinnedEventIDs: Set<String> = []
|
||||||
|
|
||||||
var timelineStartReached = false
|
var timelineStartReached = false
|
||||||
|
|
||||||
|
@ -17,6 +17,11 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
extension AttributedString {
|
extension AttributedString {
|
||||||
|
// faster than doing `String(characters)`: https://forums.swift.org/t/attributedstring-to-string/61667
|
||||||
|
var string: String {
|
||||||
|
String(characters[...])
|
||||||
|
}
|
||||||
|
|
||||||
var formattedComponents: [AttributedStringBuilderComponent] {
|
var formattedComponents: [AttributedStringBuilderComponent] {
|
||||||
runs[\.blockquote].map { value, range in
|
runs[\.blockquote].map { value, range in
|
||||||
var attributedString = AttributedString(self[range])
|
var attributedString = AttributedString(self[range])
|
||||||
|
@ -139,7 +139,7 @@ enum RoomScreenViewAction {
|
|||||||
case hasSwitchedTimeline
|
case hasSwitchedTimeline
|
||||||
|
|
||||||
case hasScrolled(direction: ScrollDirection)
|
case hasScrolled(direction: ScrollDirection)
|
||||||
case tappedPinBanner
|
case tappedPinnedEventsBanner
|
||||||
case viewAllPins
|
case viewAllPins
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -172,10 +172,14 @@ struct RoomScreenViewState: BindableState {
|
|||||||
var isPinningEnabled = false
|
var isPinningEnabled = false
|
||||||
var lastScrollDirection: ScrollDirection?
|
var lastScrollDirection: ScrollDirection?
|
||||||
|
|
||||||
|
// 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> = []
|
||||||
|
// This is used to control the banner
|
||||||
var pinnedEventsState = PinnedEventsState()
|
var pinnedEventsState = PinnedEventsState()
|
||||||
|
|
||||||
var shouldShowPinBanner: Bool {
|
var shouldShowPinnedEventsBanner: Bool {
|
||||||
isPinningEnabled && !pinnedEventsState.pinnedEventIDs.isEmpty && lastScrollDirection != .top
|
isPinningEnabled && !pinnedEventsState.pinnedEventContents.isEmpty && lastScrollDirection != .top
|
||||||
}
|
}
|
||||||
|
|
||||||
var canJoinCall = false
|
var canJoinCall = false
|
||||||
@ -296,39 +300,53 @@ enum ScrollDirection: Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct PinnedEventsState: Equatable {
|
struct PinnedEventsState: Equatable {
|
||||||
// For now these will only contain and show the event IDs, but in the future they will also contain the content
|
var pinnedEventContents: OrderedDictionary<String, AttributedString> = [:] {
|
||||||
var pinnedEventIDs: OrderedSet<String> = [] {
|
|
||||||
didSet {
|
didSet {
|
||||||
if selectedPinEventID == nil, !pinnedEventIDs.isEmpty {
|
if selectedPinEventID == nil, !pinnedEventContents.keys.isEmpty {
|
||||||
selectedPinEventID = pinnedEventIDs.first
|
selectedPinEventID = pinnedEventContents.keys.last
|
||||||
} else if pinnedEventIDs.isEmpty {
|
} else if pinnedEventContents.isEmpty {
|
||||||
selectedPinEventID = nil
|
selectedPinEventID = nil
|
||||||
} else if let selectedPinEventID, !pinnedEventIDs.contains(selectedPinEventID) {
|
} else if let selectedPinEventID, !pinnedEventContents.keys.set.contains(selectedPinEventID) {
|
||||||
self.selectedPinEventID = pinnedEventIDs.first
|
self.selectedPinEventID = pinnedEventContents.firstNonNil { $0.key }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var selectedPinEventID: String?
|
private(set) var selectedPinEventID: String?
|
||||||
|
|
||||||
var selectedPinIndex: Int {
|
var selectedPinIndex: Int {
|
||||||
|
let defaultValue = pinnedEventContents.isEmpty ? 0 : pinnedEventContents.count - 1
|
||||||
guard let selectedPinEventID else {
|
guard let selectedPinEventID else {
|
||||||
return 0
|
return defaultValue
|
||||||
}
|
}
|
||||||
return pinnedEventIDs.firstIndex(of: selectedPinEventID) ?? 0
|
return pinnedEventContents.keys.firstIndex(of: selectedPinEventID) ?? defaultValue
|
||||||
}
|
}
|
||||||
|
|
||||||
// For now we show the event ID as the content, but is just until we have a way to get the real content
|
|
||||||
var selectedPinContent: AttributedString {
|
var selectedPinContent: AttributedString {
|
||||||
.init(selectedPinEventID ?? "")
|
guard let selectedPinEventID,
|
||||||
|
var content = pinnedEventContents[selectedPinEventID] else {
|
||||||
|
return AttributedString()
|
||||||
|
}
|
||||||
|
content.font = .compound.bodyMD
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
var bannerIndicatorDescription: AttributedString {
|
||||||
|
let index = selectedPinIndex + 1
|
||||||
|
let boldPlaceholder = "{bold}"
|
||||||
|
var finalString = AttributedString(L10n.screenRoomPinnedBannerIndicatorDescription(boldPlaceholder))
|
||||||
|
var boldString = AttributedString(L10n.screenRoomPinnedBannerIndicator(index, pinnedEventContents.count))
|
||||||
|
boldString.bold()
|
||||||
|
finalString.replace(boldPlaceholder, with: boldString)
|
||||||
|
return finalString
|
||||||
}
|
}
|
||||||
|
|
||||||
mutating func nextPin() {
|
mutating func nextPin() {
|
||||||
guard !pinnedEventIDs.isEmpty else {
|
guard !pinnedEventContents.isEmpty else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let currentIndex = selectedPinIndex
|
let currentIndex = selectedPinIndex
|
||||||
let nextIndex = (currentIndex + 1) % pinnedEventIDs.count
|
let nextIndex = (currentIndex + 1) % pinnedEventContents.count
|
||||||
selectedPinEventID = pinnedEventIDs[nextIndex]
|
selectedPinEventID = pinnedEventContents.keys[nextIndex]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,6 +36,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
private let appMediator: AppMediatorProtocol
|
private let appMediator: AppMediatorProtocol
|
||||||
private let appSettings: AppSettings
|
private let appSettings: AppSettings
|
||||||
private let analyticsService: AnalyticsService
|
private let analyticsService: AnalyticsService
|
||||||
|
private let pinnedEventStringBuilder: RoomEventStringBuilder
|
||||||
|
|
||||||
private let roomScreenInteractionHandler: RoomScreenInteractionHandler
|
private let roomScreenInteractionHandler: RoomScreenInteractionHandler
|
||||||
|
|
||||||
@ -66,6 +67,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
self.analyticsService = analyticsService
|
self.analyticsService = analyticsService
|
||||||
self.userIndicatorController = userIndicatorController
|
self.userIndicatorController = userIndicatorController
|
||||||
self.appMediator = appMediator
|
self.appMediator = appMediator
|
||||||
|
pinnedEventStringBuilder = .pinnedEventStringBuilder(userID: roomProxy.ownUserID)
|
||||||
|
|
||||||
let voiceMessageRecorder = VoiceMessageRecorder(audioRecorder: AudioRecorder(), mediaPlayerProvider: mediaPlayerProvider)
|
let voiceMessageRecorder = VoiceMessageRecorder(audioRecorder: AudioRecorder(), mediaPlayerProvider: mediaPlayerProvider)
|
||||||
|
|
||||||
@ -124,6 +126,23 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
state.canJoinCall = permission
|
state.canJoinCall = permission
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Task {
|
||||||
|
guard let pinnedEventsTimelineProvider = await roomProxy.pinnedEventsTimeline?.timelineProvider else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
buildPinnedEventContent(timelineItems: pinnedEventsTimelineProvider.itemProxies)
|
||||||
|
|
||||||
|
pinnedEventsTimelineProvider.updatePublisher
|
||||||
|
// When pinning or unpinning an item, the timeline might return empty for a short while, so we need to debounce it to prevent weird UI behaviours like the banner disappearing
|
||||||
|
.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main)
|
||||||
|
.sink { [weak self] updatedItems, _ in
|
||||||
|
guard let self else { return }
|
||||||
|
buildPinnedEventContent(timelineItems: updatedItems)
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Public
|
// MARK: - Public
|
||||||
@ -196,7 +215,7 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
Task { state.timelineViewState.isSwitchingTimelines = false }
|
Task { state.timelineViewState.isSwitchingTimelines = false }
|
||||||
case let .hasScrolled(direction):
|
case let .hasScrolled(direction):
|
||||||
state.lastScrollDirection = direction
|
state.lastScrollDirection = direction
|
||||||
case .tappedPinBanner:
|
case .tappedPinnedEventsBanner:
|
||||||
if let eventID = state.pinnedEventsState.selectedPinEventID {
|
if let eventID = state.pinnedEventsState.selectedPinEventID {
|
||||||
Task { await focusOnEvent(eventID: eventID) }
|
Task { await focusOnEvent(eventID: eventID) }
|
||||||
}
|
}
|
||||||
@ -423,12 +442,12 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// If the subscription has sent a value before the Task has started it might be lost, so before entering the loop we always do an update.
|
// If the subscription has sent a value before the Task has started it might be lost, so before entering the loop we always do an update.
|
||||||
await state.pinnedEventsState.pinnedEventIDs = .init(roomProxy.pinnedEventIDs)
|
await state.pinnedEventIDs = roomProxy.pinnedEventIDs
|
||||||
for await _ in roomInfoSubscription.receive(on: DispatchQueue.main).values {
|
for await _ in roomInfoSubscription.receive(on: DispatchQueue.main).values {
|
||||||
guard !Task.isCancelled else {
|
guard !Task.isCancelled else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await state.pinnedEventsState.pinnedEventIDs = .init(roomProxy.pinnedEventIDs)
|
await state.pinnedEventIDs = roomProxy.pinnedEventIDs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
@ -635,6 +654,21 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
|
|
||||||
// MARK: - Timeline Item Building
|
// MARK: - Timeline Item Building
|
||||||
|
|
||||||
|
private func buildPinnedEventContent(timelineItems: [TimelineItemProxy]) {
|
||||||
|
var pinnedEventContents = OrderedDictionary<String, AttributedString>()
|
||||||
|
|
||||||
|
for item in timelineItems {
|
||||||
|
// Only remote events are pinned
|
||||||
|
if case let .event(event) = item,
|
||||||
|
let eventID = event.id.eventID {
|
||||||
|
pinnedEventContents.updateValue(pinnedEventStringBuilder.buildAttributedString(for: event) ?? AttributedString(L10n.commonUnsupportedEvent),
|
||||||
|
forKey: eventID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.pinnedEventsState.pinnedEventContents = pinnedEventContents
|
||||||
|
}
|
||||||
|
|
||||||
private func buildTimelineViews(timelineItems: [RoomTimelineItemProtocol], isSwitchingTimelines: Bool = false) {
|
private func buildTimelineViews(timelineItems: [RoomTimelineItemProtocol], isSwitchingTimelines: Bool = false) {
|
||||||
var timelineItemsDictionary = OrderedDictionary<String, RoomTimelineItemViewState>()
|
var timelineItemsDictionary = OrderedDictionary<String, RoomTimelineItemViewState>()
|
||||||
|
|
||||||
|
@ -23,16 +23,6 @@ struct PinnedItemsBannerView: View {
|
|||||||
let onMainButtonTap: () -> Void
|
let onMainButtonTap: () -> Void
|
||||||
let onViewAllButtonTap: () -> Void
|
let onViewAllButtonTap: () -> Void
|
||||||
|
|
||||||
private var bannerIndicatorDescription: AttributedString {
|
|
||||||
let index = pinnedEventsState.selectedPinIndex + 1
|
|
||||||
let boldPlaceholder = "{bold}"
|
|
||||||
var finalString = AttributedString(L10n.screenRoomPinnedBannerIndicatorDescription(boldPlaceholder))
|
|
||||||
var boldString = AttributedString(L10n.screenRoomPinnedBannerIndicator(index, pinnedEventsState.pinnedEventIDs.count))
|
|
||||||
boldString.bold()
|
|
||||||
finalString.replace(boldPlaceholder, with: boldString)
|
|
||||||
return finalString
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
mainButton
|
mainButton
|
||||||
@ -48,7 +38,7 @@ struct PinnedItemsBannerView: View {
|
|||||||
Button { onMainButtonTap() } label: {
|
Button { onMainButtonTap() } label: {
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
HStack(spacing: 10) {
|
HStack(spacing: 10) {
|
||||||
PinnedItemsIndicatorView(pinIndex: pinnedEventsState.selectedPinIndex, pinsCount: pinnedEventsState.pinnedEventIDs.count)
|
PinnedItemsIndicatorView(pinIndex: pinnedEventsState.selectedPinIndex, pinsCount: pinnedEventsState.pinnedEventContents.count)
|
||||||
.accessibilityHidden(true)
|
.accessibilityHidden(true)
|
||||||
CompoundIcon(\.pinSolid, size: .small, relativeTo: .compound.bodyMD)
|
CompoundIcon(\.pinSolid, size: .small, relativeTo: .compound.bodyMD)
|
||||||
.foregroundColor(Color.compound.iconSecondaryAlpha)
|
.foregroundColor(Color.compound.iconSecondaryAlpha)
|
||||||
@ -73,7 +63,7 @@ struct PinnedItemsBannerView: View {
|
|||||||
|
|
||||||
private var content: some View {
|
private var content: some View {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
Text(bannerIndicatorDescription)
|
Text(pinnedEventsState.bannerIndicatorDescription)
|
||||||
.font(.compound.bodySM)
|
.font(.compound.bodySM)
|
||||||
.foregroundColor(.compound.textActionAccent)
|
.foregroundColor(.compound.textActionAccent)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
@ -86,9 +76,32 @@ struct PinnedItemsBannerView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct PinnedItemsBannerView_Previews: PreviewProvider, TestablePreview {
|
struct PinnedItemsBannerView_Previews: PreviewProvider, TestablePreview {
|
||||||
|
static var attributedContent: AttributedString {
|
||||||
|
var boldPart = AttributedString("Image:")
|
||||||
|
boldPart.bold()
|
||||||
|
var final = boldPart + " content.png"
|
||||||
|
// This should be ignored when presented
|
||||||
|
final.font = .headline
|
||||||
|
return final
|
||||||
|
}
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
PinnedItemsBannerView(pinnedEventsState: .init(pinnedEventIDs: ["Content", "NotShown1", "NotShown2"], selectedPinEventID: "Content"),
|
VStack(spacing: 20) {
|
||||||
|
PinnedItemsBannerView(pinnedEventsState: .init(pinnedEventContents: ["1": "Content",
|
||||||
|
"2": "2",
|
||||||
|
"3": "3"],
|
||||||
|
selectedPinEventID: "1"),
|
||||||
|
onMainButtonTap: { },
|
||||||
|
onViewAllButtonTap: { })
|
||||||
|
PinnedItemsBannerView(pinnedEventsState: .init(pinnedEventContents: ["1": "Very very very very long content here",
|
||||||
|
"2": "2"],
|
||||||
|
selectedPinEventID: "1"),
|
||||||
|
onMainButtonTap: { },
|
||||||
|
onViewAllButtonTap: { })
|
||||||
|
PinnedItemsBannerView(pinnedEventsState: .init(pinnedEventContents: ["1": attributedContent],
|
||||||
|
selectedPinEventID: "1"),
|
||||||
onMainButtonTap: { },
|
onMainButtonTap: { },
|
||||||
onViewAllButtonTap: { })
|
onViewAllButtonTap: { })
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -50,11 +50,11 @@ struct RoomScreen: View {
|
|||||||
}
|
}
|
||||||
.overlay(alignment: .top) {
|
.overlay(alignment: .top) {
|
||||||
Group {
|
Group {
|
||||||
if context.viewState.shouldShowPinBanner {
|
if context.viewState.shouldShowPinnedEventsBanner {
|
||||||
pinnedItemsBanner
|
pinnedItemsBanner
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.animation(.elementDefault, value: context.viewState.shouldShowPinBanner)
|
.animation(.elementDefault, value: context.viewState.shouldShowPinnedEventsBanner)
|
||||||
}
|
}
|
||||||
.navigationTitle(L10n.screenRoomTitle) // Hidden but used for back button text.
|
.navigationTitle(L10n.screenRoomTitle) // Hidden but used for back button text.
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
@ -69,7 +69,7 @@ struct RoomScreen: View {
|
|||||||
canCurrentUserRedactSelf: context.viewState.canCurrentUserRedactSelf,
|
canCurrentUserRedactSelf: context.viewState.canCurrentUserRedactSelf,
|
||||||
canCurrentUserRedactOthers: context.viewState.canCurrentUserRedactOthers,
|
canCurrentUserRedactOthers: context.viewState.canCurrentUserRedactOthers,
|
||||||
canCurrentUserPin: context.viewState.canCurrentUserPin,
|
canCurrentUserPin: context.viewState.canCurrentUserPin,
|
||||||
pinnedEventIDs: context.viewState.pinnedEventsState.pinnedEventIDs.set,
|
pinnedEventIDs: context.viewState.pinnedEventIDs,
|
||||||
isDM: context.viewState.isEncryptedOneToOneRoom,
|
isDM: context.viewState.isEncryptedOneToOneRoom,
|
||||||
isViewSourceEnabled: context.viewState.isViewSourceEnabled).makeActions()
|
isViewSourceEnabled: context.viewState.isViewSourceEnabled).makeActions()
|
||||||
if let actions {
|
if let actions {
|
||||||
@ -111,7 +111,7 @@ struct RoomScreen: View {
|
|||||||
|
|
||||||
private var pinnedItemsBanner: some View {
|
private var pinnedItemsBanner: some View {
|
||||||
PinnedItemsBannerView(pinnedEventsState: context.viewState.pinnedEventsState,
|
PinnedItemsBannerView(pinnedEventsState: context.viewState.pinnedEventsState,
|
||||||
onMainButtonTap: { context.send(viewAction: .tappedPinBanner) },
|
onMainButtonTap: { context.send(viewAction: .tappedPinnedEventsBanner) },
|
||||||
onViewAllButtonTap: { context.send(viewAction: .viewAllPins) })
|
onViewAllButtonTap: { context.send(viewAction: .viewAllPins) })
|
||||||
.transition(.move(edge: .top))
|
.transition(.move(edge: .top))
|
||||||
}
|
}
|
||||||
|
@ -145,7 +145,7 @@ struct TimelineItemBubbledStylerView<Content: View>: View {
|
|||||||
canCurrentUserRedactSelf: context.viewState.canCurrentUserRedactSelf,
|
canCurrentUserRedactSelf: context.viewState.canCurrentUserRedactSelf,
|
||||||
canCurrentUserRedactOthers: context.viewState.canCurrentUserRedactOthers,
|
canCurrentUserRedactOthers: context.viewState.canCurrentUserRedactOthers,
|
||||||
canCurrentUserPin: context.viewState.canCurrentUserPin,
|
canCurrentUserPin: context.viewState.canCurrentUserPin,
|
||||||
pinnedEventIDs: context.viewState.pinnedEventsState.pinnedEventIDs.set,
|
pinnedEventIDs: context.viewState.pinnedEventIDs,
|
||||||
isDM: context.viewState.isEncryptedOneToOneRoom,
|
isDM: context.viewState.isEncryptedOneToOneRoom,
|
||||||
isViewSourceEnabled: context.viewState.isViewSourceEnabled)
|
isViewSourceEnabled: context.viewState.isViewSourceEnabled)
|
||||||
TimelineItemMacContextMenu(item: timelineItem, actionProvider: provider) { action in
|
TimelineItemMacContextMenu(item: timelineItem, actionProvider: provider) { action in
|
||||||
|
@ -759,10 +759,11 @@ class ClientProxy: ClientProxyProtocol {
|
|||||||
let roomListService = syncService.roomListService()
|
let roomListService = syncService.roomListService()
|
||||||
|
|
||||||
let roomMessageEventStringBuilder = RoomMessageEventStringBuilder(attributedStringBuilder: AttributedStringBuilder(cacheKey: "roomList",
|
let roomMessageEventStringBuilder = RoomMessageEventStringBuilder(attributedStringBuilder: AttributedStringBuilder(cacheKey: "roomList",
|
||||||
mentionBuilder: PlainMentionBuilder()))
|
mentionBuilder: PlainMentionBuilder()), prefix: .senderName)
|
||||||
let eventStringBuilder = RoomEventStringBuilder(stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID, shouldDisambiguateDisplayNames: false),
|
let eventStringBuilder = RoomEventStringBuilder(stateEventStringBuilder: RoomStateEventStringBuilder(userID: userID, shouldDisambiguateDisplayNames: false),
|
||||||
messageEventStringBuilder: roomMessageEventStringBuilder,
|
messageEventStringBuilder: roomMessageEventStringBuilder,
|
||||||
shouldDisambiguateDisplayNames: false)
|
shouldDisambiguateDisplayNames: false,
|
||||||
|
shouldPrefixSenderName: true)
|
||||||
|
|
||||||
roomSummaryProvider = RoomSummaryProvider(roomListService: roomListService,
|
roomSummaryProvider = RoomSummaryProvider(roomListService: roomListService,
|
||||||
eventStringBuilder: eventStringBuilder,
|
eventStringBuilder: eventStringBuilder,
|
||||||
|
@ -23,6 +23,24 @@ class RoomProxy: RoomProxyProtocol {
|
|||||||
private let roomListItem: RoomListItemProtocol
|
private let roomListItem: RoomListItemProtocol
|
||||||
private let room: RoomProtocol
|
private let room: RoomProtocol
|
||||||
let timeline: TimelineProxyProtocol
|
let timeline: TimelineProxyProtocol
|
||||||
|
private var innerPinnedEventsTimeline: TimelineProxyProtocol?
|
||||||
|
var pinnedEventsTimeline: TimelineProxyProtocol? {
|
||||||
|
get async {
|
||||||
|
if let innerPinnedEventsTimeline {
|
||||||
|
return innerPinnedEventsTimeline
|
||||||
|
} else {
|
||||||
|
do {
|
||||||
|
let timeline = try await TimelineProxy(timeline: room.pinnedEventsTimeline(internalIdPrefix: nil, maxEventsToLoad: 100), isLive: false)
|
||||||
|
await timeline.subscribeForUpdates()
|
||||||
|
innerPinnedEventsTimeline = timeline
|
||||||
|
return timeline
|
||||||
|
} catch {
|
||||||
|
MXLog.error("Failed creating pinned events timeline with error: \(error)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// periphery:ignore - required for instance retention in the rust codebase
|
// periphery:ignore - required for instance retention in the rust codebase
|
||||||
private var roomInfoObservationToken: TaskHandle?
|
private var roomInfoObservationToken: TaskHandle?
|
||||||
@ -92,9 +110,12 @@ class RoomProxy: RoomProxyProtocol {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var pinnedEventIDs: [String] {
|
var pinnedEventIDs: Set<String> {
|
||||||
get async {
|
get async {
|
||||||
await (try? room.roomInfo().pinnedEventIds) ?? []
|
guard let pinnedEventIDs = try? await room.roomInfo().pinnedEventIds else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return .init(pinnedEventIDs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,7 +38,7 @@ protocol RoomProxyProtocol {
|
|||||||
var isSpace: Bool { get }
|
var isSpace: Bool { get }
|
||||||
var isEncrypted: Bool { get }
|
var isEncrypted: Bool { get }
|
||||||
var isFavourite: Bool { get async }
|
var isFavourite: Bool { get async }
|
||||||
var pinnedEventIDs: [String] { get async }
|
var pinnedEventIDs: Set<String> { get async }
|
||||||
var membership: Membership { get }
|
var membership: Membership { get }
|
||||||
var inviter: RoomMemberProxyProtocol? { get async }
|
var inviter: RoomMemberProxyProtocol? { get async }
|
||||||
var hasOngoingCall: Bool { get }
|
var hasOngoingCall: Bool { get }
|
||||||
@ -66,6 +66,8 @@ protocol RoomProxyProtocol {
|
|||||||
|
|
||||||
var timeline: TimelineProxyProtocol { get }
|
var timeline: TimelineProxyProtocol { get }
|
||||||
|
|
||||||
|
var pinnedEventsTimeline: TimelineProxyProtocol? { get async }
|
||||||
|
|
||||||
func subscribeForUpdates() async
|
func subscribeForUpdates() async
|
||||||
|
|
||||||
func subscribeToRoomInfoUpdates()
|
func subscribeToRoomInfoUpdates()
|
||||||
|
@ -21,6 +21,7 @@ struct RoomEventStringBuilder {
|
|||||||
let stateEventStringBuilder: RoomStateEventStringBuilder
|
let stateEventStringBuilder: RoomStateEventStringBuilder
|
||||||
let messageEventStringBuilder: RoomMessageEventStringBuilder
|
let messageEventStringBuilder: RoomMessageEventStringBuilder
|
||||||
let shouldDisambiguateDisplayNames: Bool
|
let shouldDisambiguateDisplayNames: Bool
|
||||||
|
let shouldPrefixSenderName: Bool
|
||||||
|
|
||||||
func buildAttributedString(for eventItemProxy: EventTimelineItemProxy) -> AttributedString? {
|
func buildAttributedString(for eventItemProxy: EventTimelineItemProxy) -> AttributedString? {
|
||||||
let sender = eventItemProxy.sender
|
let sender = eventItemProxy.sender
|
||||||
@ -50,7 +51,7 @@ struct RoomEventStringBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let messageType = messageContent.msgtype()
|
let messageType = messageContent.msgtype()
|
||||||
return messageEventStringBuilder.buildAttributedString(for: messageType, senderDisplayName: displayName, prefixWithSenderName: true)
|
return messageEventStringBuilder.buildAttributedString(for: messageType, senderDisplayName: displayName)
|
||||||
case .state(_, let state):
|
case .state(_, let state):
|
||||||
return stateEventStringBuilder
|
return stateEventStringBuilder
|
||||||
.buildString(for: state, sender: sender, isOutgoing: isOutgoing)
|
.buildString(for: state, sender: sender, isOutgoing: isOutgoing)
|
||||||
@ -78,6 +79,9 @@ struct RoomEventStringBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func prefix(_ eventSummary: String, with senderDisplayName: String) -> AttributedString {
|
private func prefix(_ eventSummary: String, with senderDisplayName: String) -> AttributedString {
|
||||||
|
guard shouldPrefixSenderName else {
|
||||||
|
return AttributedString(eventSummary)
|
||||||
|
}
|
||||||
let attributedEventSummary = AttributedString(eventSummary.trimmingCharacters(in: .whitespacesAndNewlines))
|
let attributedEventSummary = AttributedString(eventSummary.trimmingCharacters(in: .whitespacesAndNewlines))
|
||||||
|
|
||||||
var attributedSenderDisplayName = AttributedString(senderDisplayName)
|
var attributedSenderDisplayName = AttributedString(senderDisplayName)
|
||||||
@ -86,4 +90,13 @@ struct RoomEventStringBuilder {
|
|||||||
// Don't include the message body in the markdown otherwise it makes tappable links.
|
// Don't include the message body in the markdown otherwise it makes tappable links.
|
||||||
return attributedSenderDisplayName + ": " + attributedEventSummary
|
return attributedSenderDisplayName + ": " + attributedEventSummary
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func pinnedEventStringBuilder(userID: String) -> Self {
|
||||||
|
RoomEventStringBuilder(stateEventStringBuilder: .init(userID: userID,
|
||||||
|
shouldDisambiguateDisplayNames: false),
|
||||||
|
messageEventStringBuilder: .init(attributedStringBuilder: AttributedStringBuilder(cacheKey: "pinnedEvents", mentionBuilder: PlainMentionBuilder()),
|
||||||
|
prefix: .mediaType),
|
||||||
|
shouldDisambiguateDisplayNames: false,
|
||||||
|
shouldPrefixSenderName: false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,10 +18,17 @@ import Foundation
|
|||||||
import MatrixRustSDK
|
import MatrixRustSDK
|
||||||
|
|
||||||
struct RoomMessageEventStringBuilder {
|
struct RoomMessageEventStringBuilder {
|
||||||
let attributedStringBuilder: AttributedStringBuilderProtocol
|
enum Prefix {
|
||||||
|
case senderName
|
||||||
|
case mediaType
|
||||||
|
case none
|
||||||
|
}
|
||||||
|
|
||||||
func buildAttributedString(for messageType: MessageType, senderDisplayName: String, prefixWithSenderName: Bool) -> AttributedString {
|
let attributedStringBuilder: AttributedStringBuilderProtocol
|
||||||
let message: String
|
let prefix: Prefix
|
||||||
|
|
||||||
|
func buildAttributedString(for messageType: MessageType, senderDisplayName: String) -> AttributedString {
|
||||||
|
let message: AttributedString
|
||||||
switch messageType {
|
switch messageType {
|
||||||
// Message types that don't need a prefix.
|
// Message types that don't need a prefix.
|
||||||
case .emote(content: let content):
|
case .emote(content: let content):
|
||||||
@ -33,46 +40,54 @@ struct RoomMessageEventStringBuilder {
|
|||||||
// Message types that should be prefixed with the sender's name.
|
// Message types that should be prefixed with the sender's name.
|
||||||
case .audio(content: let content):
|
case .audio(content: let content):
|
||||||
let isVoiceMessage = content.voice != nil
|
let isVoiceMessage = content.voice != nil
|
||||||
message = isVoiceMessage ? L10n.commonVoiceMessage : L10n.commonAudio
|
var content = AttributedString(isVoiceMessage ? L10n.commonVoiceMessage : L10n.commonAudio)
|
||||||
|
if prefix == .mediaType {
|
||||||
|
content.bold()
|
||||||
|
}
|
||||||
|
message = content
|
||||||
case .image(let content):
|
case .image(let content):
|
||||||
message = "\(L10n.commonImage) - \(content.body)"
|
message = prefix == .mediaType ? prefix(AttributedString(content.body), with: L10n.commonImage) : AttributedString("\(L10n.commonImage) - \(content.body)")
|
||||||
case .video(let content):
|
case .video(let content):
|
||||||
message = "\(L10n.commonVideo) - \(content.body)"
|
message = prefix == .mediaType ? prefix(AttributedString(content.body), with: L10n.commonVideo) : AttributedString("\(L10n.commonVideo) - \(content.body)")
|
||||||
case .file(let content):
|
case .file(let content):
|
||||||
message = "\(L10n.commonFile) - \(content.body)"
|
message = prefix == .mediaType ? prefix(AttributedString(content.body), with: L10n.commonFile) : AttributedString("\(L10n.commonFile) - \(content.body)")
|
||||||
case .location:
|
case .location:
|
||||||
message = L10n.commonSharedLocation
|
var content = AttributedString(L10n.commonSharedLocation)
|
||||||
|
if prefix == .mediaType {
|
||||||
|
content.bold()
|
||||||
|
}
|
||||||
|
message = content
|
||||||
case .notice(content: let content):
|
case .notice(content: let content):
|
||||||
if let attributedMessage = attributedMessageFrom(formattedBody: content.formatted) {
|
if let attributedMessage = attributedMessageFrom(formattedBody: content.formatted) {
|
||||||
message = String(attributedMessage.characters)
|
message = attributedMessage
|
||||||
} else {
|
} else {
|
||||||
message = content.body
|
message = AttributedString(content.body)
|
||||||
}
|
}
|
||||||
case .text(content: let content):
|
case .text(content: let content):
|
||||||
if let attributedMessage = attributedMessageFrom(formattedBody: content.formatted) {
|
if let attributedMessage = attributedMessageFrom(formattedBody: content.formatted) {
|
||||||
message = String(attributedMessage.characters)
|
message = attributedMessage
|
||||||
} else {
|
} else {
|
||||||
message = content.body
|
message = AttributedString(content.body)
|
||||||
}
|
}
|
||||||
case .other(_, let body):
|
case .other(_, let body):
|
||||||
message = body
|
message = AttributedString(body)
|
||||||
}
|
}
|
||||||
|
|
||||||
if prefixWithSenderName {
|
if prefix == .senderName {
|
||||||
return prefix(message, with: senderDisplayName)
|
return prefix(message, with: senderDisplayName)
|
||||||
} else {
|
} else {
|
||||||
return AttributedString(message)
|
return message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func prefix(_ eventSummary: String, with senderDisplayName: String) -> AttributedString {
|
private func prefix(_ eventSummary: AttributedString, with textToBold: String) -> AttributedString {
|
||||||
let attributedEventSummary = AttributedString(eventSummary.trimmingCharacters(in: .whitespacesAndNewlines))
|
let attributedEventSummary = AttributedString(eventSummary.string.trimmingCharacters(in: .whitespacesAndNewlines))
|
||||||
|
|
||||||
var attributedSenderDisplayName = AttributedString(senderDisplayName)
|
var attributedPrefix = AttributedString(textToBold + ":")
|
||||||
attributedSenderDisplayName.bold()
|
attributedPrefix.bold()
|
||||||
|
|
||||||
// Don't include the message body in the markdown otherwise it makes tappable links.
|
// Don't include the message body in the markdown otherwise it makes tappable links.
|
||||||
return attributedSenderDisplayName + ": " + attributedEventSummary
|
return attributedPrefix + " " + attributedEventSummary
|
||||||
}
|
}
|
||||||
|
|
||||||
private func attributedMessageFrom(formattedBody: FormattedBody?) -> AttributedString? {
|
private func attributedMessageFrom(formattedBody: FormattedBody?) -> AttributedString? {
|
||||||
|
@ -42,16 +42,17 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init(roomProxy: RoomProxyProtocol,
|
init(roomProxy: RoomProxyProtocol,
|
||||||
|
timelineProxy: TimelineProxyProtocol,
|
||||||
initialFocussedEventID: String?,
|
initialFocussedEventID: String?,
|
||||||
timelineItemFactory: RoomTimelineItemFactoryProtocol,
|
timelineItemFactory: RoomTimelineItemFactoryProtocol,
|
||||||
appSettings: AppSettings) {
|
appSettings: AppSettings) {
|
||||||
self.roomProxy = roomProxy
|
self.roomProxy = roomProxy
|
||||||
liveTimelineProvider = roomProxy.timeline.timelineProvider
|
liveTimelineProvider = timelineProxy.timelineProvider
|
||||||
self.timelineItemFactory = timelineItemFactory
|
self.timelineItemFactory = timelineItemFactory
|
||||||
self.appSettings = appSettings
|
self.appSettings = appSettings
|
||||||
serialDispatchQueue = DispatchQueue(label: "io.element.elementx.roomtimelineprovider", qos: .utility)
|
serialDispatchQueue = DispatchQueue(label: "io.element.elementx.roomtimelineprovider", qos: .utility)
|
||||||
|
|
||||||
activeTimeline = roomProxy.timeline
|
activeTimeline = timelineProxy
|
||||||
activeTimelineProvider = liveTimelineProvider
|
activeTimelineProvider = liveTimelineProvider
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange), name: UIContentSizeCategory.didChangeNotification, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange), name: UIContentSizeCategory.didChangeNotification, object: nil)
|
||||||
|
@ -21,6 +21,7 @@ struct RoomTimelineControllerFactory: RoomTimelineControllerFactoryProtocol {
|
|||||||
initialFocussedEventID: String?,
|
initialFocussedEventID: String?,
|
||||||
timelineItemFactory: RoomTimelineItemFactoryProtocol) -> RoomTimelineControllerProtocol {
|
timelineItemFactory: RoomTimelineItemFactoryProtocol) -> RoomTimelineControllerProtocol {
|
||||||
RoomTimelineController(roomProxy: roomProxy,
|
RoomTimelineController(roomProxy: roomProxy,
|
||||||
|
timelineProxy: roomProxy.timeline,
|
||||||
initialFocussedEventID: initialFocussedEventID,
|
initialFocussedEventID: initialFocussedEventID,
|
||||||
timelineItemFactory: timelineItemFactory,
|
timelineItemFactory: timelineItemFactory,
|
||||||
appSettings: ServiceLocator.shared.settings)
|
appSettings: ServiceLocator.shared.settings)
|
||||||
|
@ -634,6 +634,7 @@ class MockScreen: Identifiable {
|
|||||||
ServiceLocator.shared.settings.migratedAccounts[clientProxy.userID] = true
|
ServiceLocator.shared.settings.migratedAccounts[clientProxy.userID] = true
|
||||||
|
|
||||||
let timelineController = RoomTimelineController(roomProxy: roomProxy,
|
let timelineController = RoomTimelineController(roomProxy: roomProxy,
|
||||||
|
timelineProxy: roomProxy.timeline,
|
||||||
initialFocussedEventID: nil,
|
initialFocussedEventID: nil,
|
||||||
timelineItemFactory: RoomTimelineItemFactory(userID: "@alice:matrix.org",
|
timelineItemFactory: RoomTimelineItemFactory(userID: "@alice:matrix.org",
|
||||||
encryptionAuthenticityEnabled: true,
|
encryptionAuthenticityEnabled: true,
|
||||||
|
@ -103,7 +103,7 @@ struct NotificationContentBuilder {
|
|||||||
var notification = try await processCommonRoomMessage(notificationItem: notificationItem, mediaProvider: mediaProvider)
|
var notification = try await processCommonRoomMessage(notificationItem: notificationItem, mediaProvider: mediaProvider)
|
||||||
|
|
||||||
let displayName = notificationItem.senderDisplayName ?? notificationItem.roomDisplayName
|
let displayName = notificationItem.senderDisplayName ?? notificationItem.roomDisplayName
|
||||||
let message = String(messageEventStringBuilder.buildAttributedString(for: messageType, senderDisplayName: displayName, prefixWithSenderName: false).characters)
|
let message = String(messageEventStringBuilder.buildAttributedString(for: messageType, senderDisplayName: displayName).characters)
|
||||||
|
|
||||||
notification.body = notificationItem.hasMention ? L10n.notificationMentionedYouBody(message) : message
|
notification.body = notificationItem.hasMention ? L10n.notificationMentionedYouBody(message) : message
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ import UserNotifications
|
|||||||
// database, logging, etc. are only ever setup once per *process*
|
// database, logging, etc. are only ever setup once per *process*
|
||||||
|
|
||||||
private let settings: NSESettingsProtocol = AppSettings()
|
private let settings: NSESettingsProtocol = AppSettings()
|
||||||
private let notificationContentBuilder = NotificationContentBuilder(messageEventStringBuilder: RoomMessageEventStringBuilder(attributedStringBuilder: AttributedStringBuilder(mentionBuilder: PlainMentionBuilder())))
|
private let notificationContentBuilder = NotificationContentBuilder(messageEventStringBuilder: RoomMessageEventStringBuilder(attributedStringBuilder: AttributedStringBuilder(mentionBuilder: PlainMentionBuilder()), prefix: .none))
|
||||||
private let keychainController = KeychainController(service: .sessions,
|
private let keychainController = KeychainController(service: .sessions,
|
||||||
accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier)
|
accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier)
|
||||||
|
|
||||||
|
BIN
PreviewTests/__Snapshots__/PreviewTests/test_pinnedItemsBannerView-iPad-en-GB.1.png
(Stored with Git LFS)
BIN
PreviewTests/__Snapshots__/PreviewTests/test_pinnedItemsBannerView-iPad-en-GB.1.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/__Snapshots__/PreviewTests/test_pinnedItemsBannerView-iPad-pseudo.1.png
(Stored with Git LFS)
BIN
PreviewTests/__Snapshots__/PreviewTests/test_pinnedItemsBannerView-iPad-pseudo.1.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/__Snapshots__/PreviewTests/test_pinnedItemsBannerView-iPhone-15-en-GB.1.png
(Stored with Git LFS)
BIN
PreviewTests/__Snapshots__/PreviewTests/test_pinnedItemsBannerView-iPhone-15-en-GB.1.png
(Stored with Git LFS)
Binary file not shown.
BIN
PreviewTests/__Snapshots__/PreviewTests/test_pinnedItemsBannerView-iPhone-15-pseudo.1.png
(Stored with Git LFS)
BIN
PreviewTests/__Snapshots__/PreviewTests/test_pinnedItemsBannerView-iPhone-15-pseudo.1.png
(Stored with Git LFS)
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user