Detect links in room detail topics and make it more obvious when the text is truncated (#2213)

This commit is contained in:
Stefan Ceriu 2023-12-12 15:52:10 +02:00 committed by GitHub
parent b9a6ebe03a
commit 9ff1deaf73
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 125 additions and 39 deletions

View File

@ -498,7 +498,9 @@ class RoomFlowCoordinator: FlowCoordinatorProtocol {
roomProxy: roomProxy,
mediaProvider: userSession.mediaProvider,
userIndicatorController: userIndicatorController,
notificationSettings: userSession.clientProxy.notificationSettings)
notificationSettings: userSession.clientProxy.notificationSettings,
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: appSettings.permalinkBaseURL,
mentionBuilder: MentionBuilder()))
let coordinator = RoomDetailsScreenCoordinator(parameters: params)
coordinator.actions.sink { [weak self] action in
guard let self else { return }

View File

@ -23,6 +23,7 @@ struct RoomDetailsScreenCoordinatorParameters {
let mediaProvider: MediaProviderProtocol
let userIndicatorController: UserIndicatorControllerProtocol
let notificationSettings: NotificationSettingsProxyProtocol
let attributedStringBuilder: AttributedStringBuilderProtocol
}
enum RoomDetailsScreenCoordinatorAction {
@ -51,7 +52,8 @@ final class RoomDetailsScreenCoordinator: CoordinatorProtocol {
roomProxy: parameters.roomProxy,
mediaProvider: parameters.mediaProvider,
userIndicatorController: parameters.userIndicatorController,
notificationSettingsProxy: parameters.notificationSettings)
notificationSettingsProxy: parameters.notificationSettings,
attributedStringBuilder: parameters.attributedStringBuilder)
}
// MARK: - Public

View File

@ -39,7 +39,8 @@ struct RoomDetailsScreenViewState: BindableState {
let permalink: URL?
var title = ""
var topic: String?
var topic: AttributedString?
var topicSummary: AttributedString?
var avatarURL: URL?
var joinedMembersCount: Int
var isProcessingIgnoreRequest = false

View File

@ -25,6 +25,7 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
private let mediaProvider: MediaProviderProtocol
private let userIndicatorController: UserIndicatorControllerProtocol
private let notificationSettingsProxy: NotificationSettingsProxyProtocol
private let attributedStringBuilder: AttributedStringBuilderProtocol
private var accountOwner: RoomMemberProxyProtocol? {
didSet { updatePowerLevelPermissions() }
@ -42,12 +43,16 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
roomProxy: RoomProxyProtocol,
mediaProvider: MediaProviderProtocol,
userIndicatorController: UserIndicatorControllerProtocol,
notificationSettingsProxy: NotificationSettingsProxyProtocol) {
notificationSettingsProxy: NotificationSettingsProxyProtocol,
attributedStringBuilder: AttributedStringBuilderProtocol) {
self.accountUserID = accountUserID
self.roomProxy = roomProxy
self.mediaProvider = mediaProvider
self.userIndicatorController = userIndicatorController
self.notificationSettingsProxy = notificationSettingsProxy
self.attributedStringBuilder = attributedStringBuilder
let topic = attributedStringBuilder.fromPlain(roomProxy.topic)
super.init(initialViewState: .init(roomId: roomProxy.id,
canonicalAlias: roomProxy.canonicalAlias,
@ -55,7 +60,8 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
isDirect: roomProxy.isDirect,
permalink: roomProxy.permalink,
title: roomProxy.roomTitle,
topic: roomProxy.topic,
topic: topic,
topicSummary: topic?.unattributedStringByReplacingNewlinesWithSpaces(),
avatarURL: roomProxy.avatarURL,
joinedMembersCount: roomProxy.joinedMembersCount,
notificationSettingsState: .loading,
@ -126,7 +132,10 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
.sink { [weak self] _ in
guard let self else { return }
self.state.title = self.roomProxy.roomTitle
self.state.topic = self.roomProxy.topic
let topic = attributedStringBuilder.fromPlain(self.roomProxy.topic)
self.state.topic = topic
self.state.topicSummary = topic?.unattributedStringByReplacingNewlinesWithSpaces()
self.state.avatarURL = self.roomProxy.avatarURL
self.state.joinedMembersCount = self.roomProxy.joinedMembersCount
}
@ -299,3 +308,10 @@ class RoomDetailsScreenViewModel: RoomDetailsScreenViewModelType, RoomDetailsScr
}
}
}
private extension AttributedString {
/// Returns a new string without attributes and in which newlines are replaced with spaces
func unattributedStringByReplacingNewlinesWithSpaces() -> AttributedString {
AttributedString(characters.map { $0.isNewline ? " " : $0 })
}
}

View File

@ -123,10 +123,19 @@ struct RoomDetailsScreen: View {
private var topicSection: some View {
if context.viewState.hasTopicSection {
Section {
if let topic = context.viewState.topic, !topic.isEmpty {
ListRow(label: .description(topic), kind: .label)
.lineLimit(isTopicExpanded ? nil : 3)
.onTapGesture { isTopicExpanded.toggle() }
if let topic = context.viewState.topic, !topic.characters.isEmpty, let topicSummary = context.viewState.topicSummary {
ListRow(kind: .custom {
Text(isTopicExpanded ? topic : topicSummary)
.font(.compound.bodySM)
.foregroundColor(.compound.textSecondary)
.lineLimit(isTopicExpanded ? nil : 3)
.accentColor(.compound.textLinkExternal)
.padding(ListRowPadding.insets)
.textSelection(.enabled)
})
.onTapGesture {
isTopicExpanded.toggle()
}
} else {
ListRow(label: .plain(title: L10n.screenRoomDetailsAddTopicTitle),
kind: .button { context.send(viewAction: .processTapAddTopic) })
@ -264,7 +273,14 @@ struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview {
.mockCharlie
]
let roomProxy = RoomProxyMock(with: .init(id: "room_a_id", displayName: "Room A",
topic: "Bacon ipsum dolor amet short ribs buffalo pork loin cupim frankfurter. Burgdoggen pig shankle biltong flank ham jowl sirloin bacon cow. T-bone alcatra boudin beef spare ribs pig fatback jerky swine short ribs shankle chislic frankfurter pork loin. Chicken tri-tip bresaola t-bone pastrami brisket.", // swiftlint:disable:this line_length
topic: """
Discussions about Element X iOS | https://github.com/vector-im/element-x-ios
Feature Status: https://github.com/vector-im/element-x-ios/issues/1225
App Store: https://apple.co/3r6LJHZ
TestFlight: https://testflight.apple.com/join/uZbeZCOi
""",
isDirect: false,
isEncrypted: true,
canonicalAlias: "#alias:domain.com",
@ -279,7 +295,9 @@ struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview {
roomProxy: roomProxy,
mediaProvider: MockMediaProvider(),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: notificationSettingsProxy)
notificationSettingsProxy: notificationSettingsProxy,
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: .userDirectory,
mentionBuilder: MentionBuilder()))
}()
static let dmRoomViewModel = {
@ -302,7 +320,9 @@ struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview {
roomProxy: roomProxy,
mediaProvider: MockMediaProvider(),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: notificationSettingsProxy)
notificationSettingsProxy: notificationSettingsProxy,
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: .userDirectory,
mentionBuilder: MentionBuilder()))
}()
static let simpleRoomViewModel = {
@ -322,7 +342,9 @@ struct RoomDetailsScreen_Previews: PreviewProvider, TestablePreview {
roomProxy: roomProxy,
mediaProvider: MockMediaProvider(),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: notificationSettingsProxy)
notificationSettingsProxy: notificationSettingsProxy,
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: .userDirectory,
mentionBuilder: MentionBuilder()))
}()
static var previews: some View {

View File

@ -589,7 +589,9 @@ class MockScreen: Identifiable {
roomProxy: roomProxy,
mediaProvider: MockMediaProvider(),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettings: NotificationSettingsProxyMock(with: .init())))
notificationSettings: NotificationSettingsProxyMock(with: .init()),
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL,
mentionBuilder: MentionBuilder())))
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
case .roomDetailsScreenWithRoomAvatar:
@ -608,7 +610,9 @@ class MockScreen: Identifiable {
roomProxy: roomProxy,
mediaProvider: MockMediaProvider(),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettings: NotificationSettingsProxyMock(with: .init())))
notificationSettings: NotificationSettingsProxyMock(with: .init()),
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL,
mentionBuilder: MentionBuilder())))
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
case .roomDetailsScreenWithEmptyTopic:
@ -629,7 +633,9 @@ class MockScreen: Identifiable {
roomProxy: roomProxy,
mediaProvider: MockMediaProvider(),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettings: NotificationSettingsProxyMock(with: .init())))
notificationSettings: NotificationSettingsProxyMock(with: .init()),
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL,
mentionBuilder: MentionBuilder())))
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
case .roomDetailsScreenWithInvite:
@ -646,7 +652,9 @@ class MockScreen: Identifiable {
roomProxy: roomProxy,
mediaProvider: MockMediaProvider(),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettings: NotificationSettingsProxyMock(with: .init())))
notificationSettings: NotificationSettingsProxyMock(with: .init()),
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL,
mentionBuilder: MentionBuilder())))
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
case .roomDetailsScreenDmDetails:
@ -664,7 +672,9 @@ class MockScreen: Identifiable {
roomProxy: roomProxy,
mediaProvider: MockMediaProvider(),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettings: NotificationSettingsProxyMock(with: .init())))
notificationSettings: NotificationSettingsProxyMock(with: .init()),
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL,
mentionBuilder: MentionBuilder())))
navigationStackCoordinator.setRootCoordinator(coordinator)
return navigationStackCoordinator
case .roomEditDetails, .roomEditDetailsReadOnly:

View File

@ -37,7 +37,9 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
roomProxy: roomProxyMock,
mediaProvider: MockMediaProvider(),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: notificationSettingsProxyMock)
notificationSettingsProxy: notificationSettingsProxyMock,
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL,
mentionBuilder: MentionBuilder()))
AppSettings.reset()
}
@ -49,7 +51,9 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
roomProxy: roomProxyMock,
mediaProvider: MockMediaProvider(),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()))
notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()),
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL,
mentionBuilder: MentionBuilder()))
let deferred = deferFulfillment(context.$viewState) { state in
state.bindings.leaveRoomAlertItem != nil
}
@ -68,7 +72,9 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
roomProxy: roomProxyMock,
mediaProvider: MockMediaProvider(),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()))
notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()),
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL,
mentionBuilder: MentionBuilder()))
let deferred = deferFulfillment(context.$viewState) { state in
state.bindings.leaveRoomAlertItem != nil
}
@ -130,7 +136,9 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
roomProxy: roomProxyMock,
mediaProvider: MockMediaProvider(),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()))
notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()),
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL,
mentionBuilder: MentionBuilder()))
let deferred = deferFulfillment(viewModel.context.$viewState) { state in
state.dmRecipient != nil
@ -154,7 +162,9 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
roomProxy: roomProxyMock,
mediaProvider: MockMediaProvider(),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()))
notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()),
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL,
mentionBuilder: MentionBuilder()))
var deferred = deferFulfillment(viewModel.context.$viewState) { state in
state.dmRecipient != nil
@ -187,7 +197,9 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
roomProxy: roomProxyMock,
mediaProvider: MockMediaProvider(),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()))
notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()),
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL,
mentionBuilder: MentionBuilder()))
var deferred = deferFulfillment(viewModel.context.$viewState) { state in
state.dmRecipient != nil
@ -221,7 +233,9 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
roomProxy: roomProxyMock,
mediaProvider: MockMediaProvider(),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()))
notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()),
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL,
mentionBuilder: MentionBuilder()))
var deferred = deferFulfillment(viewModel.context.$viewState) { state in
state.dmRecipient != nil
@ -254,7 +268,9 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
roomProxy: roomProxyMock,
mediaProvider: MockMediaProvider(),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()))
notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()),
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL,
mentionBuilder: MentionBuilder()))
var deferred = deferFulfillment(viewModel.context.$viewState) { state in
state.dmRecipient != nil
@ -287,7 +303,9 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
roomProxy: roomProxyMock,
mediaProvider: MockMediaProvider(),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()))
notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()),
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL,
mentionBuilder: MentionBuilder()))
_ = await context.$viewState.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main).values.first()
@ -301,7 +319,9 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
roomProxy: roomProxyMock,
mediaProvider: MockMediaProvider(),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()))
notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()),
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL,
mentionBuilder: MentionBuilder()))
_ = await context.$viewState.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main).values.first()
@ -332,7 +352,9 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
roomProxy: roomProxyMock,
mediaProvider: MockMediaProvider(),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()))
notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()),
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL,
mentionBuilder: MentionBuilder()))
_ = await context.$viewState.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main).values.first()
@ -350,7 +372,9 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
roomProxy: roomProxyMock,
mediaProvider: MockMediaProvider(),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()))
notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()),
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL,
mentionBuilder: MentionBuilder()))
_ = await context.$viewState.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main).values.first()
@ -368,7 +392,9 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
roomProxy: roomProxyMock,
mediaProvider: MockMediaProvider(),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()))
notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()),
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL,
mentionBuilder: MentionBuilder()))
_ = await context.$viewState.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main).values.first()
@ -386,7 +412,9 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
roomProxy: roomProxyMock,
mediaProvider: MockMediaProvider(),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()))
notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()),
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL,
mentionBuilder: MentionBuilder()))
_ = await context.$viewState.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main).values.first()
@ -403,7 +431,9 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
roomProxy: roomProxyMock,
mediaProvider: MockMediaProvider(),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()))
notificationSettingsProxy: NotificationSettingsProxyMock(with: NotificationSettingsProxyMockConfiguration()),
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL,
mentionBuilder: MentionBuilder()))
_ = await context.$viewState.debounce(for: .milliseconds(100), scheduler: DispatchQueue.main).values.first()
@ -418,7 +448,9 @@ class RoomDetailsScreenViewModelTests: XCTestCase {
roomProxy: roomProxyMock,
mediaProvider: MockMediaProvider(),
userIndicatorController: ServiceLocator.shared.userIndicatorController,
notificationSettingsProxy: notificationSettingsProxyMock)
notificationSettingsProxy: notificationSettingsProxyMock,
attributedStringBuilder: AttributedStringBuilder(permalinkBaseURL: ServiceLocator.shared.settings.permalinkBaseURL,
mentionBuilder: MentionBuilder()))
var deferred = deferFulfillment(context.$viewState) { state in
state.notificationSettingsState.isError

1
changelog.d/2210.bugfix Normal file
View File

@ -0,0 +1 @@
Detect links in room detail topics and make it more obvious when the text is truncated