mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-05 11:58:32 +00:00
Knocked Preview implementation (#3426)
* JoinRoomScreen ui for knocking * code improvement * updated previews * added knocked state with tests * send knock request * Apply suggestions from code review Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com> * pr comments * update tests * new API * knock implementation and cancel knock * update strings * added a knocked cell in the home screen * design update * updated SDK * simplified the invite case code * pr comments * updated previews * added message as reason * updated strings * fixing tests --------- Co-authored-by: Doug <6060466+pixlwave@users.noreply.github.com>
This commit is contained in:
@@ -371,6 +371,7 @@
|
||||
"screen_room_pinned_banner_loading_description" = "Loading message…";
|
||||
"screen_room_pinned_banner_view_all_button_title" = "View All";
|
||||
"screen_room_details_pinned_events_row_title" = "Pinned messages";
|
||||
"screen_roomlist_knock_event_sent_description" = "Request to join sent";
|
||||
"screen_timeline_item_menu_send_failure_changed_identity" = "Message not sent because %1$@’s verified identity has changed.";
|
||||
"screen_timeline_item_menu_send_failure_unsigned_device" = "Message not sent because %1$@ has not verified all devices.";
|
||||
"screen_timeline_item_menu_send_failure_you_unsigned_device" = "Message not sent because you have not verified one or more of your devices.";
|
||||
@@ -822,12 +823,16 @@
|
||||
"screen_session_verification_open_existing_session_title" = "Open an existing session";
|
||||
"screen_session_verification_positive_button_canceled" = "Retry verification";
|
||||
"screen_session_verification_positive_button_initial" = "I am ready";
|
||||
"screen_session_verification_positive_button_verifying_ongoing" = "Waiting to match";
|
||||
"screen_session_verification_positive_button_verifying_ongoing" = "Waiting to match…";
|
||||
"screen_session_verification_ready_subtitle" = "Compare a unique set of emojis.";
|
||||
"screen_session_verification_request_accepted_subtitle" = "Compare the unique emoji, ensuring they appear in the same order.";
|
||||
"screen_session_verification_request_details_timestamp" = "Signed in";
|
||||
"screen_session_verification_request_failure_subtitle" = "Either the request timed out, the request was denied, or there was a verification mismatch.";
|
||||
"screen_session_verification_request_failure_title" = "Verification failed";
|
||||
"screen_session_verification_request_footer" = "Only continue if you initiated this verification.";
|
||||
"screen_session_verification_request_subtitle" = "Verify the other device to keep your message history secure.";
|
||||
"screen_session_verification_request_success_subtitle" = "Now you can read or send messages securely on your other device.";
|
||||
"screen_session_verification_request_success_title" = "Device verified";
|
||||
"screen_session_verification_request_title" = "Verification requested";
|
||||
"screen_session_verification_they_dont_match" = "They don’t match";
|
||||
"screen_session_verification_they_match" = "They match";
|
||||
|
||||
@@ -1965,6 +1965,8 @@ internal enum L10n {
|
||||
/// Congrats!
|
||||
/// You don’t have any unread messages!
|
||||
internal static var screenRoomlistFilterUnreadsEmptyStateTitle: String { return L10n.tr("Localizable", "screen_roomlist_filter_unreads_empty_state_title") }
|
||||
/// Request to join sent
|
||||
internal static var screenRoomlistKnockEventSentDescription: String { return L10n.tr("Localizable", "screen_roomlist_knock_event_sent_description") }
|
||||
/// Chats
|
||||
internal static var screenRoomlistMainSpaceTitle: String { return L10n.tr("Localizable", "screen_roomlist_main_space_title") }
|
||||
/// Mark as read
|
||||
@@ -2013,7 +2015,7 @@ internal enum L10n {
|
||||
internal static var screenSessionVerificationPositiveButtonCanceled: String { return L10n.tr("Localizable", "screen_session_verification_positive_button_canceled") }
|
||||
/// I am ready
|
||||
internal static var screenSessionVerificationPositiveButtonInitial: String { return L10n.tr("Localizable", "screen_session_verification_positive_button_initial") }
|
||||
/// Waiting to match
|
||||
/// Waiting to match…
|
||||
internal static var screenSessionVerificationPositiveButtonVerifyingOngoing: String { return L10n.tr("Localizable", "screen_session_verification_positive_button_verifying_ongoing") }
|
||||
/// Compare a unique set of emojis.
|
||||
internal static var screenSessionVerificationReadySubtitle: String { return L10n.tr("Localizable", "screen_session_verification_ready_subtitle") }
|
||||
@@ -2021,10 +2023,18 @@ internal enum L10n {
|
||||
internal static var screenSessionVerificationRequestAcceptedSubtitle: String { return L10n.tr("Localizable", "screen_session_verification_request_accepted_subtitle") }
|
||||
/// Signed in
|
||||
internal static var screenSessionVerificationRequestDetailsTimestamp: String { return L10n.tr("Localizable", "screen_session_verification_request_details_timestamp") }
|
||||
/// Either the request timed out, the request was denied, or there was a verification mismatch.
|
||||
internal static var screenSessionVerificationRequestFailureSubtitle: String { return L10n.tr("Localizable", "screen_session_verification_request_failure_subtitle") }
|
||||
/// Verification failed
|
||||
internal static var screenSessionVerificationRequestFailureTitle: String { return L10n.tr("Localizable", "screen_session_verification_request_failure_title") }
|
||||
/// Only continue if you initiated this verification.
|
||||
internal static var screenSessionVerificationRequestFooter: String { return L10n.tr("Localizable", "screen_session_verification_request_footer") }
|
||||
/// Verify the other device to keep your message history secure.
|
||||
internal static var screenSessionVerificationRequestSubtitle: String { return L10n.tr("Localizable", "screen_session_verification_request_subtitle") }
|
||||
/// Now you can read or send messages securely on your other device.
|
||||
internal static var screenSessionVerificationRequestSuccessSubtitle: String { return L10n.tr("Localizable", "screen_session_verification_request_success_subtitle") }
|
||||
/// Device verified
|
||||
internal static var screenSessionVerificationRequestSuccessTitle: String { return L10n.tr("Localizable", "screen_session_verification_request_success_title") }
|
||||
/// Verification requested
|
||||
internal static var screenSessionVerificationRequestTitle: String { return L10n.tr("Localizable", "screen_session_verification_request_title") }
|
||||
/// They don’t match
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
//
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Foundation
|
||||
|
||||
@MainActor
|
||||
struct InvitedRoomProxyMockConfiguration {
|
||||
var id = UUID().uuidString
|
||||
var name: String?
|
||||
var avatarURL: URL?
|
||||
var members: [RoomMemberProxyMock] = .allMembers
|
||||
var inviter: RoomMemberProxyMock = .mockAlice
|
||||
}
|
||||
|
||||
extension InvitedRoomProxyMock {
|
||||
@MainActor
|
||||
convenience init(_ configuration: InvitedRoomProxyMockConfiguration) {
|
||||
self.init()
|
||||
id = configuration.id
|
||||
name = configuration.name
|
||||
avatarURL = configuration.avatarURL
|
||||
avatar = .room(id: configuration.id, name: configuration.name, avatarURL: configuration.avatarURL) // Note: This doesn't replicate the real proxy logic.
|
||||
underlyingInviter = configuration.inviter
|
||||
activeMembersCount = configuration.members.filter { $0.membership == .join || $0.membership == .invite }.count
|
||||
}
|
||||
}
|
||||
@@ -71,8 +71,7 @@ extension Array where Element == RoomSummary {
|
||||
static let mockRooms: [Element] = [
|
||||
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||
id: "1",
|
||||
isInvite: false,
|
||||
inviter: nil,
|
||||
joinRequestType: nil,
|
||||
name: "Foundation 🔭🪐🌌",
|
||||
isDirect: false,
|
||||
avatarURL: nil,
|
||||
@@ -89,8 +88,7 @@ extension Array where Element == RoomSummary {
|
||||
isFavourite: false),
|
||||
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||
id: "2",
|
||||
isInvite: false,
|
||||
inviter: nil,
|
||||
joinRequestType: nil,
|
||||
name: "Foundation and Empire",
|
||||
isDirect: false,
|
||||
avatarURL: URL.picturesDirectory,
|
||||
@@ -107,8 +105,7 @@ extension Array where Element == RoomSummary {
|
||||
isFavourite: false),
|
||||
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||
id: "3",
|
||||
isInvite: false,
|
||||
inviter: nil,
|
||||
joinRequestType: nil,
|
||||
name: "Second Foundation",
|
||||
isDirect: false,
|
||||
avatarURL: nil,
|
||||
@@ -125,8 +122,7 @@ extension Array where Element == RoomSummary {
|
||||
isFavourite: false),
|
||||
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||
id: "4",
|
||||
isInvite: false,
|
||||
inviter: nil,
|
||||
joinRequestType: nil,
|
||||
name: "Foundation's Edge",
|
||||
isDirect: false,
|
||||
avatarURL: nil,
|
||||
@@ -143,8 +139,7 @@ extension Array where Element == RoomSummary {
|
||||
isFavourite: false),
|
||||
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||
id: "5",
|
||||
isInvite: false,
|
||||
inviter: nil,
|
||||
joinRequestType: nil,
|
||||
name: "Foundation and Earth",
|
||||
isDirect: true,
|
||||
avatarURL: nil,
|
||||
@@ -161,8 +156,7 @@ extension Array where Element == RoomSummary {
|
||||
isFavourite: false),
|
||||
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||
id: "6",
|
||||
isInvite: false,
|
||||
inviter: nil,
|
||||
joinRequestType: nil,
|
||||
name: "Prelude to Foundation",
|
||||
isDirect: true,
|
||||
avatarURL: nil,
|
||||
@@ -179,8 +173,7 @@ extension Array where Element == RoomSummary {
|
||||
isFavourite: false),
|
||||
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||
id: "0",
|
||||
isInvite: false,
|
||||
inviter: nil,
|
||||
joinRequestType: nil,
|
||||
name: "Unknown",
|
||||
isDirect: false,
|
||||
avatarURL: nil,
|
||||
@@ -230,8 +223,7 @@ extension Array where Element == RoomSummary {
|
||||
static let mockInvites: [Element] = [
|
||||
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||
id: "someAwesomeRoomId1",
|
||||
isInvite: false,
|
||||
inviter: RoomMemberProxyMock.mockCharlie,
|
||||
joinRequestType: .invite(inviter: RoomMemberProxyMock.mockCharlie),
|
||||
name: "First room",
|
||||
isDirect: false,
|
||||
avatarURL: URL.picturesDirectory,
|
||||
@@ -248,8 +240,7 @@ extension Array where Element == RoomSummary {
|
||||
isFavourite: false),
|
||||
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||
id: "someAwesomeRoomId2",
|
||||
isInvite: false,
|
||||
inviter: RoomMemberProxyMock.mockCharlie,
|
||||
joinRequestType: .invite(inviter: RoomMemberProxyMock.mockCharlie),
|
||||
name: "Second room",
|
||||
isDirect: true,
|
||||
avatarURL: nil,
|
||||
|
||||
@@ -127,10 +127,11 @@ struct HomeScreenViewStateBindings {
|
||||
}
|
||||
|
||||
struct HomeScreenRoom: Identifiable, Equatable {
|
||||
enum RoomType {
|
||||
enum RoomType: Equatable {
|
||||
case placeholder
|
||||
case room
|
||||
case invite
|
||||
case invite(inviterDetails: RoomInviterDetails?)
|
||||
case knock
|
||||
}
|
||||
|
||||
static let placeholderLastMessage = AttributedString("Hidden last message")
|
||||
@@ -143,6 +144,13 @@ struct HomeScreenRoom: Identifiable, Equatable {
|
||||
|
||||
let type: RoomType
|
||||
|
||||
var inviter: RoomInviterDetails? {
|
||||
if case .invite(let inviter) = type {
|
||||
return inviter
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
let badges: Badges
|
||||
struct Badges: Equatable {
|
||||
let isDotShown: Bool
|
||||
@@ -164,9 +172,7 @@ struct HomeScreenRoom: Identifiable, Equatable {
|
||||
let lastMessage: AttributedString?
|
||||
|
||||
let avatar: RoomAvatar
|
||||
|
||||
let inviter: RoomInviterDetails?
|
||||
|
||||
|
||||
let canonicalAlias: String?
|
||||
|
||||
static func placeholder() -> HomeScreenRoom {
|
||||
@@ -181,7 +187,6 @@ struct HomeScreenRoom: Identifiable, Equatable {
|
||||
timestamp: "Now",
|
||||
lastMessage: placeholderLastMessage,
|
||||
avatar: .room(id: "", name: "", avatarURL: nil),
|
||||
inviter: nil,
|
||||
canonicalAlias: nil)
|
||||
}
|
||||
}
|
||||
@@ -192,17 +197,21 @@ extension HomeScreenRoom {
|
||||
|
||||
let hasUnreadMessages = hideUnreadMessagesBadge ? false : summary.hasUnreadMessages
|
||||
|
||||
let isDotShown = hasUnreadMessages || summary.hasUnreadMentions || summary.hasUnreadNotifications || summary.isMarkedUnread
|
||||
let isDotShown = hasUnreadMessages || summary.hasUnreadMentions || summary.hasUnreadNotifications || summary.isMarkedUnread || summary.joinRequestType?.isKnock == true
|
||||
let isMentionShown = summary.hasUnreadMentions && !summary.isMuted
|
||||
let isMuteShown = summary.isMuted
|
||||
let isCallShown = summary.hasOngoingCall
|
||||
let isHighlighted = summary.isMarkedUnread || (!summary.isMuted && (summary.hasUnreadNotifications || summary.hasUnreadMentions))
|
||||
let isHighlighted = summary.isMarkedUnread || (!summary.isMuted && (summary.hasUnreadNotifications || summary.hasUnreadMentions)) || summary.joinRequestType?.isKnock == true
|
||||
|
||||
let inviter = summary.inviter.map(RoomInviterDetails.init)
|
||||
let type: HomeScreenRoom.RoomType = switch summary.joinRequestType {
|
||||
case .invite(let inviter): .invite(inviterDetails: inviter.map(RoomInviterDetails.init))
|
||||
case .knock: .knock
|
||||
case .none: .room
|
||||
}
|
||||
|
||||
self.init(id: identifier,
|
||||
roomID: summary.id,
|
||||
type: summary.isInvite ? .invite : .room,
|
||||
type: type,
|
||||
badges: .init(isDotShown: isDotShown,
|
||||
isMentionShown: isMentionShown,
|
||||
isMuteShown: isMuteShown,
|
||||
@@ -214,7 +223,6 @@ extension HomeScreenRoom {
|
||||
timestamp: summary.lastMessageFormattedTimestamp,
|
||||
lastMessage: summary.lastMessage,
|
||||
avatar: summary.avatar,
|
||||
inviter: inviter,
|
||||
canonicalAlias: summary.canonicalAlias)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,8 @@ struct HomeScreenInviteCell: View {
|
||||
|
||||
@ViewBuilder
|
||||
private var inviterView: some View {
|
||||
if let inviter = room.inviter, !room.isDirect {
|
||||
if let inviter = room.inviter,
|
||||
!room.isDirect {
|
||||
RoomInviterLabel(inviter: inviter, mediaProvider: context.mediaProvider)
|
||||
.font(.compound.bodyMD)
|
||||
.foregroundStyle(.compound.textPlaceholder)
|
||||
@@ -177,8 +178,7 @@ private extension HomeScreenRoom {
|
||||
|
||||
let summary = RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||
id: "@someone:somewhere.com",
|
||||
isInvite: false,
|
||||
inviter: inviter,
|
||||
joinRequestType: .invite(inviter: inviter),
|
||||
name: "Some Guy",
|
||||
isDirect: true,
|
||||
avatarURL: nil,
|
||||
@@ -205,8 +205,7 @@ private extension HomeScreenRoom {
|
||||
|
||||
let summary = RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||
id: "@someone:somewhere.com",
|
||||
isInvite: false,
|
||||
inviter: inviter,
|
||||
joinRequestType: .invite(inviter: inviter),
|
||||
name: "Awesome Room",
|
||||
isDirect: false,
|
||||
avatarURL: avatarURL,
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
//
|
||||
// Copyright 2024 New Vector Ltd.
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// Please see LICENSE in the repository root for full details.
|
||||
//
|
||||
|
||||
import Combine
|
||||
import Compound
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
struct HomeScreenKnockedCell: View {
|
||||
@Environment(\.dynamicTypeSize) var dynamicTypeSize
|
||||
|
||||
let room: HomeScreenRoom
|
||||
let context: HomeScreenViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .top, spacing: 16) {
|
||||
if dynamicTypeSize < .accessibility3 {
|
||||
RoomAvatarImage(avatar: room.avatar,
|
||||
avatarSize: .custom(52),
|
||||
mediaProvider: context.mediaProvider)
|
||||
.dynamicTypeSize(dynamicTypeSize < .accessibility1 ? dynamicTypeSize : .accessibility1)
|
||||
.accessibilityHidden(true)
|
||||
}
|
||||
|
||||
mainContent
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.bottom, 16)
|
||||
.padding(.trailing, 16)
|
||||
.multilineTextAlignment(.leading)
|
||||
.overlay(alignment: .bottom) {
|
||||
separator
|
||||
}
|
||||
}
|
||||
.padding(.top, 12)
|
||||
.padding(.leading, 16)
|
||||
.onTapGesture {
|
||||
if let roomID = room.roomID {
|
||||
context.send(viewAction: .selectRoom(roomIdentifier: roomID))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private var mainContent: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 16) {
|
||||
textualContent
|
||||
badge
|
||||
}
|
||||
|
||||
Text(L10n.screenRoomlistKnockEventSentDescription)
|
||||
.font(.compound.bodyMD)
|
||||
.foregroundStyle(.compound.textPlaceholder)
|
||||
.padding(.top, room.canonicalAlias == nil ? 0 : 4)
|
||||
.padding(.trailing, 16)
|
||||
}
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var textualContent: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
Text(title)
|
||||
.font(.compound.bodyLGSemibold)
|
||||
.foregroundColor(.compound.textPrimary)
|
||||
.lineLimit(2)
|
||||
|
||||
if let subtitle {
|
||||
Text(subtitle)
|
||||
.font(.compound.bodyMD)
|
||||
.foregroundColor(.compound.textPlaceholder)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
||||
private var separator: some View {
|
||||
Rectangle()
|
||||
.fill(Color.compound.borderDisabled)
|
||||
.frame(height: 1 / UIScreen.main.scale)
|
||||
}
|
||||
|
||||
private var title: String {
|
||||
room.name
|
||||
}
|
||||
|
||||
private var subtitle: String? {
|
||||
room.canonicalAlias
|
||||
}
|
||||
|
||||
private var badge: some View {
|
||||
Circle()
|
||||
.scaledFrame(size: 12)
|
||||
.foregroundColor(.compound.iconAccentTertiary)
|
||||
}
|
||||
}
|
||||
|
||||
struct HomeScreenKnockedCell_Previews: PreviewProvider, TestablePreview {
|
||||
static var previews: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: 0) {
|
||||
HomeScreenKnockedCell(room: .dmInvite,
|
||||
context: viewModel().context)
|
||||
|
||||
HomeScreenKnockedCell(room: .dmInvite,
|
||||
context: viewModel().context)
|
||||
|
||||
HomeScreenKnockedCell(room: .roomKnocked(),
|
||||
context: viewModel().context)
|
||||
|
||||
HomeScreenKnockedCell(room: .roomKnocked(),
|
||||
context: viewModel().context)
|
||||
|
||||
HomeScreenKnockedCell(room: .roomKnocked(alias: "#footest:somewhere.org", avatarURL: .picturesDirectory),
|
||||
context: viewModel().context)
|
||||
|
||||
HomeScreenKnockedCell(room: .roomKnocked(alias: "#footest:somewhere.org"),
|
||||
context: viewModel().context)
|
||||
.dynamicTypeSize(.accessibility1)
|
||||
.previewDisplayName("Aliased room (AX1)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func viewModel() -> HomeScreenViewModel {
|
||||
let clientProxy = ClientProxyMock(.init())
|
||||
|
||||
let userSession = UserSessionMock(.init(clientProxy: clientProxy))
|
||||
|
||||
return HomeScreenViewModel(userSession: userSession,
|
||||
analyticsService: ServiceLocator.shared.analytics,
|
||||
appSettings: ServiceLocator.shared.settings,
|
||||
selectedRoomPublisher: CurrentValueSubject<String?, Never>(nil).asCurrentValuePublisher(),
|
||||
userIndicatorController: ServiceLocator.shared.userIndicatorController)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private extension HomeScreenRoom {
|
||||
static var dmInvite: HomeScreenRoom {
|
||||
let inviter = RoomMemberProxyMock()
|
||||
inviter.displayName = "Jack"
|
||||
inviter.userID = "@jack:somewhere.com"
|
||||
|
||||
let summary = RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||
id: "@someone:somewhere.com",
|
||||
joinRequestType: .invite(inviter: inviter),
|
||||
name: "Some Guy",
|
||||
isDirect: true,
|
||||
avatarURL: nil,
|
||||
heroes: [.init(userID: "@someone:somewhere.com")],
|
||||
lastMessage: nil,
|
||||
lastMessageFormattedTimestamp: nil,
|
||||
unreadMessagesCount: 0,
|
||||
unreadMentionsCount: 0,
|
||||
unreadNotificationsCount: 0,
|
||||
notificationMode: nil,
|
||||
canonicalAlias: "#footest:somewhere.org",
|
||||
hasOngoingCall: false,
|
||||
isMarkedUnread: false,
|
||||
isFavourite: false)
|
||||
|
||||
return .init(summary: summary, hideUnreadMessagesBadge: false)
|
||||
}
|
||||
|
||||
static func roomKnocked(alias: String? = nil, avatarURL: URL? = nil) -> HomeScreenRoom {
|
||||
let inviter = RoomMemberProxyMock()
|
||||
inviter.displayName = "Luca"
|
||||
inviter.userID = "@jack:somewhi.nl"
|
||||
inviter.avatarURL = avatarURL
|
||||
|
||||
let summary = RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||
id: "@someone:somewhere.com",
|
||||
joinRequestType: .invite(inviter: inviter),
|
||||
name: "Awesome Room",
|
||||
isDirect: false,
|
||||
avatarURL: avatarURL,
|
||||
heroes: [.init(userID: "@someone:somewhere.com")],
|
||||
lastMessage: nil,
|
||||
lastMessageFormattedTimestamp: nil,
|
||||
unreadMessagesCount: 0,
|
||||
unreadMentionsCount: 0,
|
||||
unreadNotificationsCount: 0,
|
||||
notificationMode: nil,
|
||||
canonicalAlias: alias,
|
||||
hasOngoingCall: false,
|
||||
isMarkedUnread: false,
|
||||
isFavourite: false)
|
||||
|
||||
return .init(summary: summary, hideUnreadMessagesBadge: false)
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,8 @@ struct HomeScreenRoomList: View {
|
||||
.redacted(reason: .placeholder)
|
||||
case .invite:
|
||||
HomeScreenInviteCell(room: room, context: context)
|
||||
case .knock:
|
||||
HomeScreenKnockedCell(room: room, context: context)
|
||||
case .room:
|
||||
let isSelected = context.viewState.selectedRoomID == room.id
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ final class JoinRoomScreenCoordinator: CoordinatorProtocol {
|
||||
switch action {
|
||||
case .joined:
|
||||
actionsSubject.send(.joined)
|
||||
case .cancelled:
|
||||
case .dismiss:
|
||||
actionsSubject.send(.cancelled)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import Foundation
|
||||
|
||||
enum JoinRoomScreenViewModelAction {
|
||||
case joined
|
||||
case cancelled
|
||||
case dismiss
|
||||
}
|
||||
|
||||
enum JoinRoomScreenInteractionMode {
|
||||
@@ -65,6 +65,7 @@ struct JoinRoomScreenViewStateBindings {
|
||||
|
||||
enum JoinRoomScreenAlertType {
|
||||
case declineInvite
|
||||
case cancelKnock
|
||||
}
|
||||
|
||||
enum JoinRoomScreenViewAction {
|
||||
|
||||
@@ -59,8 +59,7 @@ class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewMo
|
||||
case .declineInvite:
|
||||
showDeclineInviteConfirmationAlert()
|
||||
case .cancelKnock:
|
||||
// TODO: implement once available
|
||||
break
|
||||
showCancelKnockConfirmationAlert()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,15 +77,7 @@ class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewMo
|
||||
Task { await updateRoomDetails() }
|
||||
}
|
||||
|
||||
// Using only the preview API isn't enough as it's not capable
|
||||
// of giving us information for non-joined rooms (at least not on synapse)
|
||||
// See if we known about the room locally and, if so, have that
|
||||
// take priority over the preview one.
|
||||
|
||||
if let room = await clientProxy.roomForIdentifier(roomID) {
|
||||
self.room = room
|
||||
await updateRoomDetails()
|
||||
}
|
||||
await updateRoom()
|
||||
|
||||
switch await clientProxy.roomPreviewForIdentifier(roomID, via: via) {
|
||||
case .success(let roomPreviewDetails):
|
||||
@@ -99,6 +90,17 @@ class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewMo
|
||||
}
|
||||
}
|
||||
|
||||
private func updateRoom() async {
|
||||
// Using only the preview API isn't enough as it's not capable
|
||||
// of giving us information for non-joined rooms (at least not on synapse)
|
||||
// See if we known about the room locally and, if so, have that
|
||||
// take priority over the preview one.
|
||||
if let room = await clientProxy.roomForIdentifier(roomID) {
|
||||
self.room = room
|
||||
await updateRoomDetails()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateRoomDetails() async {
|
||||
var roomProxy: RoomProxyProtocol?
|
||||
var inviter: RoomInviterDetails?
|
||||
@@ -188,7 +190,8 @@ class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewMo
|
||||
switch await clientProxy.knockRoomAlias(alias,
|
||||
message: state.bindings.knockMessage.isBlank ? nil : state.bindings.knockMessage) {
|
||||
case .success:
|
||||
state.mode = .knocked
|
||||
// The room should become knocked through the sync
|
||||
await updateRoom()
|
||||
case .failure(let error):
|
||||
MXLog.error("Failed knocking room alias: \(alias) with error: \(error)")
|
||||
userIndicatorController.submitIndicator(.init(title: L10n.errorUnknown))
|
||||
@@ -198,7 +201,8 @@ class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewMo
|
||||
via: via,
|
||||
message: state.bindings.knockMessage.isBlank ? nil : state.bindings.knockMessage) {
|
||||
case .success:
|
||||
state.mode = .knocked
|
||||
// The room should become knocked through the sync
|
||||
await updateRoom()
|
||||
case .failure(let error):
|
||||
MXLog.error("Failed knocking room id: \(roomID) with error: \(error)")
|
||||
userIndicatorController.submitIndicator(.init(title: L10n.errorUnknown))
|
||||
@@ -220,6 +224,14 @@ class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewMo
|
||||
secondaryButton: .init(title: L10n.actionDecline, role: .destructive, action: { Task { await self.declineInvite() } }))
|
||||
}
|
||||
|
||||
private func showCancelKnockConfirmationAlert() {
|
||||
state.bindings.alertInfo = .init(id: .cancelKnock,
|
||||
title: L10n.screenJoinRoomCancelKnockAlertTitle,
|
||||
message: L10n.screenJoinRoomCancelKnockAlertDescription,
|
||||
primaryButton: .init(title: L10n.actionNo, role: .cancel, action: nil),
|
||||
secondaryButton: .init(title: L10n.screenJoinRoomCancelKnockAlertConfirmation, role: .destructive, action: { Task { await self.cancelKnock() } }))
|
||||
}
|
||||
|
||||
private func declineInvite() async {
|
||||
defer {
|
||||
userIndicatorController.retractIndicatorWithId(roomID)
|
||||
@@ -236,6 +248,29 @@ class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewMo
|
||||
|
||||
if case .failure = result {
|
||||
userIndicatorController.submitIndicator(.init(title: L10n.errorUnknown))
|
||||
} else {
|
||||
actionsSubject.send(.dismiss)
|
||||
}
|
||||
}
|
||||
|
||||
private func cancelKnock() async {
|
||||
defer {
|
||||
userIndicatorController.retractIndicatorWithId(roomID)
|
||||
}
|
||||
|
||||
userIndicatorController.submitIndicator(UserIndicator(id: roomID, type: .modal, title: L10n.commonLoading, persistent: true))
|
||||
|
||||
guard case let .knocked(roomProxy) = room else {
|
||||
userIndicatorController.submitIndicator(.init(title: L10n.errorUnknown))
|
||||
return
|
||||
}
|
||||
|
||||
let result = await roomProxy.cancelKnock()
|
||||
|
||||
if case .failure = result {
|
||||
userIndicatorController.submitIndicator(.init(title: L10n.errorUnknown))
|
||||
} else {
|
||||
actionsSubject.send(.dismiss)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ struct JoinRoomScreen: View {
|
||||
@ObservedObject var context: JoinRoomScreenViewModel.Context
|
||||
|
||||
var body: some View {
|
||||
FullscreenDialog(topPadding: 80, background: .bloom) {
|
||||
FullscreenDialog(topPadding: context.viewState.mode == .knocked ? 151 : 35, background: .bloom) {
|
||||
if context.viewState.mode == .loading {
|
||||
EmptyView()
|
||||
} else {
|
||||
@@ -27,13 +27,15 @@ struct JoinRoomScreen: View {
|
||||
.background()
|
||||
.backgroundStyle(.compound.bgCanvasDefault)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar { toolbar }
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
var mainContent: some View {
|
||||
if context.viewState.mode == .knocked {
|
||||
switch context.viewState.mode {
|
||||
case .knocked:
|
||||
knockedView
|
||||
} else {
|
||||
default:
|
||||
defaultView
|
||||
}
|
||||
}
|
||||
@@ -54,7 +56,7 @@ struct JoinRoomScreen: View {
|
||||
|
||||
if let subtitle = context.viewState.subtitle {
|
||||
Text(subtitle)
|
||||
.font(.compound.bodyMD)
|
||||
.font(.compound.bodyLG)
|
||||
.foregroundStyle(.compound.textSecondary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
@@ -101,7 +103,7 @@ struct JoinRoomScreen: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ViewBuilder
|
||||
private var knockMessage: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
@@ -158,6 +160,17 @@ struct JoinRoomScreen: View {
|
||||
Button(L10n.actionAccept) { context.send(viewAction: .acceptInvite) }
|
||||
.buttonStyle(.compound(.primary))
|
||||
}
|
||||
|
||||
@ToolbarContentBuilder
|
||||
private var toolbar: some ToolbarContent {
|
||||
if context.viewState.mode == .knocked {
|
||||
ToolbarItem(placement: .principal) {
|
||||
RoomHeaderView(roomName: context.viewState.title,
|
||||
roomAvatar: context.viewState.avatar,
|
||||
mediaProvider: context.mediaProvider)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
@@ -221,10 +234,17 @@ struct JoinRoomScreen_Previews: PreviewProvider, TestablePreview {
|
||||
if mode == .unknown {
|
||||
clientProxy.roomPreviewForIdentifierViaReturnValue = .failure(.sdkError(ClientProxyMockError.generic))
|
||||
} else {
|
||||
if mode == .knocked {
|
||||
switch mode {
|
||||
case .knocked:
|
||||
clientProxy.roomForIdentifierClosure = { _ in
|
||||
.knocked(KnockedRoomProxyMock(.init()))
|
||||
.knocked(KnockedRoomProxyMock(.init(avatarURL: URL.homeDirectory)))
|
||||
}
|
||||
case .invited:
|
||||
clientProxy.roomForIdentifierClosure = { _ in
|
||||
.invited(InvitedRoomProxyMock(.init(avatarURL: URL.homeDirectory)))
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
clientProxy.roomPreviewForIdentifierViaReturnValue = .success(.init(roomID: "1",
|
||||
name: "The Three-Body Problem - 三体",
|
||||
|
||||
@@ -417,7 +417,7 @@ class ClientProxy: ClientProxyProtocol {
|
||||
|
||||
func knockRoom(_ roomID: String, via: [String], message: String?) async -> Result<Void, ClientProxyError> {
|
||||
do {
|
||||
let _ = try await client.knock(roomIdOrAlias: roomID, reason: nil, serverNames: via)
|
||||
let _ = try await client.knock(roomIdOrAlias: roomID, reason: message, serverNames: via)
|
||||
await waitForRoomToSync(roomID: roomID, timeout: .seconds(30))
|
||||
return .success(())
|
||||
} catch {
|
||||
@@ -428,7 +428,7 @@ class ClientProxy: ClientProxyProtocol {
|
||||
|
||||
func knockRoomAlias(_ roomAlias: String, message: String?) async -> Result<Void, ClientProxyError> {
|
||||
do {
|
||||
let room = try await client.knock(roomIdOrAlias: roomAlias, reason: nil, serverNames: [])
|
||||
let room = try await client.knock(roomIdOrAlias: roomAlias, reason: message, serverNames: [])
|
||||
await waitForRoomToSync(roomID: room.id(), timeout: .seconds(30))
|
||||
return .success(())
|
||||
} catch {
|
||||
|
||||
@@ -76,7 +76,11 @@ class KnockedRoomProxy: KnockedRoomProxyProtocol {
|
||||
}
|
||||
|
||||
func cancelKnock() async -> Result<Void, RoomProxyError> {
|
||||
// TODO: Implement this once the API is available
|
||||
.failure(.invalidURL)
|
||||
do {
|
||||
return try await .success(room.leave())
|
||||
} catch {
|
||||
MXLog.error("Failed cancelling the knock with error: \(error)")
|
||||
return .failure(.sdkError(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,12 +9,32 @@ import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
struct RoomSummary {
|
||||
enum JoinRequestType {
|
||||
case invite(inviter: RoomMemberProxyProtocol?)
|
||||
case knock
|
||||
|
||||
var isInvite: Bool {
|
||||
if case .invite = self {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var isKnock: Bool {
|
||||
if case .knock = self {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let roomListItem: RoomListItem
|
||||
|
||||
let id: String
|
||||
|
||||
let isInvite: Bool
|
||||
let inviter: RoomMemberProxyProtocol?
|
||||
let joinRequestType: JoinRequestType?
|
||||
|
||||
let name: String
|
||||
let isDirect: Bool
|
||||
@@ -67,10 +87,9 @@ extension RoomSummary {
|
||||
unreadNotificationsCount = hasUnreadNotifications ? 1 : 0
|
||||
notificationMode = settingsMode
|
||||
canonicalAlias = nil
|
||||
inviter = nil
|
||||
hasOngoingCall = false
|
||||
|
||||
isInvite = false
|
||||
joinRequestType = nil
|
||||
isMarkedUnread = false
|
||||
isFavourite = false
|
||||
}
|
||||
|
||||
@@ -255,10 +255,15 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||
|
||||
let notificationMode = roomInfo.cachedUserDefinedNotificationMode.flatMap { RoomNotificationModeProxy.from(roomNotificationMode: $0) }
|
||||
|
||||
let joinRequestType: RoomSummary.JoinRequestType? = switch roomInfo.membership {
|
||||
case .invited: .invite(inviter: inviterProxy)
|
||||
case .knocked: .knock
|
||||
default: nil
|
||||
}
|
||||
|
||||
return RoomSummary(roomListItem: roomListItem,
|
||||
id: roomInfo.id,
|
||||
isInvite: roomInfo.membership == .invited,
|
||||
inviter: inviterProxy,
|
||||
joinRequestType: joinRequestType,
|
||||
name: roomInfo.displayName ?? roomInfo.id,
|
||||
isDirect: roomInfo.isDirect,
|
||||
avatarURL: roomInfo.avatarUrl.flatMap(URL.init(string:)),
|
||||
|
||||
@@ -18,7 +18,6 @@ enum TimelineKind {
|
||||
enum TimelineProxyError: Error {
|
||||
case sdkError(Error)
|
||||
|
||||
case failedEditing
|
||||
case failedRedacting
|
||||
case failedPaginatingEndReached
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user