mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 19:44:05 +00:00
Hide the unread dot after previewing an invite. (#3800)
* Hide the unread dot when previewing an invite. * Remove an invited room ID when accepting/rejecting. * Remove the unread badge from knocked room cells. * Update snapshots. * Address PR comments. Refactor KnockRequestType to JoinRequestType.
This commit is contained in:
@@ -20,6 +20,7 @@ protocol CommonSettingsProtocol {
|
||||
final class AppSettings {
|
||||
private enum UserDefaultsKeys: String {
|
||||
case lastVersionLaunched
|
||||
case seenInvites
|
||||
case appLockNumberOfPINAttempts
|
||||
case appLockNumberOfBiometricAttempts
|
||||
case timelineStyle
|
||||
@@ -104,6 +105,11 @@ final class AppSettings {
|
||||
@UserPreference(key: UserDefaultsKeys.lastVersionLaunched, storageType: .userDefaults(store))
|
||||
var lastVersionLaunched: String?
|
||||
|
||||
/// The Set of room identifiers of invites that the user already saw in the invites list.
|
||||
/// This Set is being used to implement badges for unread invites.
|
||||
@UserPreference(key: UserDefaultsKeys.seenInvites, defaultValue: [], storageType: .userDefaults(store))
|
||||
var seenInvites: Set<String>
|
||||
|
||||
/// The default homeserver address used. This is intentionally a string without a scheme
|
||||
/// so that it can be passed to Rust as a ServerName for well-known discovery.
|
||||
private(set) var defaultHomeserverAddress = "matrix.org"
|
||||
|
||||
@@ -25,6 +25,8 @@ extension InvitedRoomProxyMock {
|
||||
id = configuration.id
|
||||
inviter = configuration.inviter
|
||||
info = RoomInfoProxy(roomInfo: .init(configuration))
|
||||
|
||||
rejectInvitationReturnValue = .success(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ extension Array where Element == RoomSummary {
|
||||
static let mockRooms: [Element] = [
|
||||
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||
id: "1",
|
||||
knockRequestType: nil,
|
||||
joinRequestType: nil,
|
||||
name: "Foundation 🔭🪐🌌",
|
||||
isDirect: false,
|
||||
avatarURL: nil,
|
||||
@@ -88,7 +88,7 @@ extension Array where Element == RoomSummary {
|
||||
isFavourite: false),
|
||||
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||
id: "2",
|
||||
knockRequestType: nil,
|
||||
joinRequestType: nil,
|
||||
name: "Foundation and Empire",
|
||||
isDirect: false,
|
||||
avatarURL: .mockMXCAvatar,
|
||||
@@ -105,7 +105,7 @@ extension Array where Element == RoomSummary {
|
||||
isFavourite: false),
|
||||
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||
id: "3",
|
||||
knockRequestType: nil,
|
||||
joinRequestType: nil,
|
||||
name: "Second Foundation",
|
||||
isDirect: false,
|
||||
avatarURL: nil,
|
||||
@@ -122,7 +122,7 @@ extension Array where Element == RoomSummary {
|
||||
isFavourite: false),
|
||||
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||
id: "4",
|
||||
knockRequestType: nil,
|
||||
joinRequestType: nil,
|
||||
name: "Foundation's Edge",
|
||||
isDirect: false,
|
||||
avatarURL: nil,
|
||||
@@ -139,7 +139,7 @@ extension Array where Element == RoomSummary {
|
||||
isFavourite: false),
|
||||
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||
id: "5",
|
||||
knockRequestType: nil,
|
||||
joinRequestType: nil,
|
||||
name: "Foundation and Earth",
|
||||
isDirect: true,
|
||||
avatarURL: nil,
|
||||
@@ -156,7 +156,7 @@ extension Array where Element == RoomSummary {
|
||||
isFavourite: false),
|
||||
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||
id: "6",
|
||||
knockRequestType: nil,
|
||||
joinRequestType: nil,
|
||||
name: "Prelude to Foundation",
|
||||
isDirect: true,
|
||||
avatarURL: nil,
|
||||
@@ -173,7 +173,7 @@ extension Array where Element == RoomSummary {
|
||||
isFavourite: false),
|
||||
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||
id: "0",
|
||||
knockRequestType: nil,
|
||||
joinRequestType: nil,
|
||||
name: "Unknown",
|
||||
isDirect: false,
|
||||
avatarURL: nil,
|
||||
@@ -223,7 +223,7 @@ extension Array where Element == RoomSummary {
|
||||
static let mockInvites: [Element] = [
|
||||
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||
id: "someAwesomeRoomId1",
|
||||
knockRequestType: .invite(inviter: RoomMemberProxyMock.mockCharlie),
|
||||
joinRequestType: .invite(inviter: RoomMemberProxyMock.mockCharlie),
|
||||
name: "First room",
|
||||
isDirect: false,
|
||||
avatarURL: .mockMXCAvatar,
|
||||
@@ -240,7 +240,7 @@ extension Array where Element == RoomSummary {
|
||||
isFavourite: false),
|
||||
RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||
id: "someAwesomeRoomId2",
|
||||
knockRequestType: .invite(inviter: RoomMemberProxyMock.mockCharlie),
|
||||
joinRequestType: .invite(inviter: RoomMemberProxyMock.mockCharlie),
|
||||
name: "Second room",
|
||||
isDirect: true,
|
||||
avatarURL: nil,
|
||||
|
||||
@@ -9,7 +9,7 @@ import Combine
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
enum HomeScreenViewModelAction {
|
||||
enum HomeScreenViewModelAction: Equatable {
|
||||
case presentRoom(roomIdentifier: String)
|
||||
case presentRoomDetails(roomIdentifier: String)
|
||||
case roomLeft(roomIdentifier: String)
|
||||
@@ -207,24 +207,25 @@ struct HomeScreenRoom: Identifiable, Equatable {
|
||||
}
|
||||
|
||||
extension HomeScreenRoom {
|
||||
init(summary: RoomSummary, hideUnreadMessagesBadge: Bool) {
|
||||
let identifier = summary.id
|
||||
init(summary: RoomSummary, hideUnreadMessagesBadge: Bool, seenInvites: Set<String> = []) {
|
||||
let roomID = summary.id
|
||||
|
||||
let hasUnreadMessages = hideUnreadMessagesBadge ? false : summary.hasUnreadMessages
|
||||
let isUnseenInvite = summary.joinRequestType?.isInvite == true && !seenInvites.contains(roomID)
|
||||
|
||||
let isDotShown = hasUnreadMessages || summary.hasUnreadMentions || summary.hasUnreadNotifications || summary.isMarkedUnread || summary.knockRequestType?.isKnock == true
|
||||
let isDotShown = hasUnreadMessages || summary.hasUnreadMentions || summary.hasUnreadNotifications || summary.isMarkedUnread || isUnseenInvite
|
||||
let isMentionShown = summary.hasUnreadMentions && !summary.isMuted
|
||||
let isMuteShown = summary.isMuted
|
||||
let isCallShown = summary.hasOngoingCall
|
||||
let isHighlighted = summary.isMarkedUnread || (!summary.isMuted && (summary.hasUnreadNotifications || summary.hasUnreadMentions)) || summary.knockRequestType?.isKnock == true
|
||||
let isHighlighted = summary.isMarkedUnread || (!summary.isMuted && (summary.hasUnreadNotifications || summary.hasUnreadMentions)) || isUnseenInvite
|
||||
|
||||
let type: HomeScreenRoom.RoomType = switch summary.knockRequestType {
|
||||
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,
|
||||
self.init(id: roomID,
|
||||
roomID: summary.id,
|
||||
type: type,
|
||||
badges: .init(isDotShown: isDotShown,
|
||||
|
||||
@@ -98,6 +98,13 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
.weakAssign(to: \.state.isRoomDirectorySearchEnabled, on: self)
|
||||
.store(in: &cancellables)
|
||||
|
||||
appSettings.$seenInvites
|
||||
.removeDuplicates()
|
||||
.sink { [weak self] _ in
|
||||
self?.updateRooms()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
let isSearchFieldFocused = context.$viewState.map(\.bindings.isSearchFieldFocused)
|
||||
let searchQuery = context.$viewState.map(\.bindings.searchQuery)
|
||||
let activeFilters = context.$viewState.map(\.bindings.filtersState.activeFilters)
|
||||
@@ -290,9 +297,12 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
}
|
||||
|
||||
var rooms = [HomeScreenRoom]()
|
||||
let seenInvites = appSettings.seenInvites
|
||||
|
||||
for summary in roomSummaryProvider.roomListPublisher.value {
|
||||
let room = HomeScreenRoom(summary: summary, hideUnreadMessagesBadge: appSettings.hideUnreadMessagesBadge)
|
||||
let room = HomeScreenRoom(summary: summary,
|
||||
hideUnreadMessagesBadge: appSettings.hideUnreadMessagesBadge,
|
||||
seenInvites: seenInvites)
|
||||
rooms.append(room)
|
||||
}
|
||||
|
||||
@@ -396,6 +406,7 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
analyticsService.trackJoinedRoom(isDM: roomProxy.info.isDirect,
|
||||
isSpace: roomProxy.info.isSpace,
|
||||
activeMemberCount: UInt(roomProxy.info.activeMembersCount))
|
||||
appSettings.seenInvites.remove(roomID)
|
||||
case .failure:
|
||||
displayError()
|
||||
}
|
||||
@@ -414,8 +425,8 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
state.bindings.alertInfo = .init(id: UUID(),
|
||||
title: title,
|
||||
message: message,
|
||||
primaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil),
|
||||
secondaryButton: .init(title: L10n.actionDecline, role: .destructive) { Task { await self.declineInvite(roomID: room.id) } })
|
||||
primaryButton: .init(title: L10n.actionDecline, role: .destructive) { Task { await self.declineInvite(roomID: room.id) } },
|
||||
secondaryButton: .init(title: L10n.actionCancel, role: .cancel, action: nil))
|
||||
}
|
||||
|
||||
private func declineInvite(roomID: String) async {
|
||||
@@ -432,7 +443,10 @@ class HomeScreenViewModel: HomeScreenViewModelType, HomeScreenViewModelProtocol
|
||||
|
||||
let result = await roomProxy.rejectInvitation()
|
||||
|
||||
if case .failure = result {
|
||||
switch result {
|
||||
case .success:
|
||||
appSettings.seenInvites.remove(roomID)
|
||||
case .failure:
|
||||
displayError()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,10 +122,13 @@ struct HomeScreenInviteCell: View {
|
||||
room.isDirect ? room.inviter?.id : room.canonicalAlias
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var badge: some View {
|
||||
Circle()
|
||||
.scaledFrame(size: 12)
|
||||
.foregroundColor(.compound.iconAccentTertiary)
|
||||
if room.badges.isDotShown {
|
||||
Circle()
|
||||
.scaledFrame(size: 12)
|
||||
.foregroundColor(.compound.iconAccentTertiary) // The badge is always green, no need to check isHighlighted here.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +181,7 @@ private extension HomeScreenRoom {
|
||||
|
||||
let summary = RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||
id: "@someone:somewhere.com",
|
||||
knockRequestType: .invite(inviter: inviter),
|
||||
joinRequestType: .invite(inviter: inviter),
|
||||
name: "Some Guy",
|
||||
isDirect: true,
|
||||
avatarURL: nil,
|
||||
@@ -205,7 +208,7 @@ private extension HomeScreenRoom {
|
||||
|
||||
let summary = RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||
id: "@someone:somewhere.com",
|
||||
knockRequestType: .invite(inviter: inviter),
|
||||
joinRequestType: .invite(inviter: inviter),
|
||||
name: "Awesome Room",
|
||||
isDirect: false,
|
||||
avatarURL: avatarURL,
|
||||
|
||||
@@ -49,10 +49,9 @@ struct HomeScreenKnockedCell: View {
|
||||
private var mainContent: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 16) {
|
||||
textualContent
|
||||
badge
|
||||
}
|
||||
textualContent
|
||||
|
||||
// No badge - the user initiated the knock, it cannot be unread.
|
||||
|
||||
Text(L10n.screenRoomlistKnockEventSentDescription)
|
||||
.font(.compound.bodyMD)
|
||||
@@ -95,12 +94,6 @@ struct HomeScreenKnockedCell: View {
|
||||
private var subtitle: String? {
|
||||
room.canonicalAlias
|
||||
}
|
||||
|
||||
private var badge: some View {
|
||||
Circle()
|
||||
.scaledFrame(size: 12)
|
||||
.foregroundColor(.compound.iconAccentTertiary)
|
||||
}
|
||||
}
|
||||
|
||||
struct HomeScreenKnockedCell_Previews: PreviewProvider, TestablePreview {
|
||||
@@ -152,7 +145,7 @@ private extension HomeScreenRoom {
|
||||
|
||||
let summary = RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||
id: "@someone:somewhere.com",
|
||||
knockRequestType: .invite(inviter: inviter),
|
||||
joinRequestType: .invite(inviter: inviter),
|
||||
name: "Some Guy",
|
||||
isDirect: true,
|
||||
avatarURL: nil,
|
||||
@@ -179,7 +172,7 @@ private extension HomeScreenRoom {
|
||||
|
||||
let summary = RoomSummary(roomListItem: RoomListItemSDKMock(),
|
||||
id: "@someone:somewhere.com",
|
||||
knockRequestType: .invite(inviter: inviter),
|
||||
joinRequestType: .invite(inviter: inviter),
|
||||
name: "Awesome Room",
|
||||
isDirect: false,
|
||||
avatarURL: avatarURL,
|
||||
|
||||
@@ -41,6 +41,18 @@ class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewMo
|
||||
|
||||
super.init(initialViewState: JoinRoomScreenViewState(roomID: roomID), mediaProvider: mediaProvider)
|
||||
|
||||
context.$viewState.map(\.mode)
|
||||
.removeDuplicates()
|
||||
.sink { mode in
|
||||
switch mode {
|
||||
case .invited:
|
||||
appSettings.seenInvites.insert(roomID)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
Task {
|
||||
await loadRoomDetails()
|
||||
}
|
||||
@@ -225,6 +237,7 @@ class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewMo
|
||||
if let alias = state.roomDetails?.canonicalAlias {
|
||||
switch await clientProxy.joinRoomAlias(alias) {
|
||||
case .success:
|
||||
appSettings.seenInvites.remove(roomID)
|
||||
actionsSubject.send(.joined)
|
||||
case .failure(let error):
|
||||
if case .forbiddenAccess = error {
|
||||
@@ -238,6 +251,7 @@ class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewMo
|
||||
} else {
|
||||
switch await clientProxy.joinRoom(roomID, via: via) {
|
||||
case .success:
|
||||
appSettings.seenInvites.remove(roomID)
|
||||
actionsSubject.send(.joined)
|
||||
case .failure(let error):
|
||||
if case .forbiddenAccess = error {
|
||||
@@ -343,6 +357,8 @@ class JoinRoomScreenViewModel: JoinRoomScreenViewModelType, JoinRoomScreenViewMo
|
||||
return false
|
||||
}
|
||||
|
||||
appSettings.seenInvites.remove(roomID)
|
||||
|
||||
actionsSubject.send(.dismiss)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -9,23 +9,21 @@ import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
struct RoomSummary {
|
||||
enum KnockRequestType {
|
||||
enum JoinRequestType {
|
||||
case invite(inviter: RoomMemberProxyProtocol?)
|
||||
case knock
|
||||
|
||||
var isInvite: Bool {
|
||||
if case .invite = self {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
switch self {
|
||||
case .invite: true
|
||||
default: false
|
||||
}
|
||||
}
|
||||
|
||||
var isKnock: Bool {
|
||||
if case .knock = self {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
switch self {
|
||||
case .knock: true
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,7 +32,7 @@ struct RoomSummary {
|
||||
|
||||
let id: String
|
||||
|
||||
let knockRequestType: KnockRequestType?
|
||||
let joinRequestType: JoinRequestType?
|
||||
|
||||
let name: String
|
||||
let isDirect: Bool
|
||||
@@ -103,7 +101,7 @@ extension RoomSummary {
|
||||
canonicalAlias = nil
|
||||
hasOngoingCall = false
|
||||
|
||||
knockRequestType = nil
|
||||
joinRequestType = nil
|
||||
isMarkedUnread = false
|
||||
isFavourite = false
|
||||
}
|
||||
|
||||
@@ -255,7 +255,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||
|
||||
let notificationMode = roomInfo.cachedUserDefinedNotificationMode.flatMap { RoomNotificationModeProxy.from(roomNotificationMode: $0) }
|
||||
|
||||
let knockRequestType: RoomSummary.KnockRequestType? = switch roomInfo.membership {
|
||||
let joinRequestType: RoomSummary.JoinRequestType? = switch roomInfo.membership {
|
||||
case .invited: .invite(inviter: inviterProxy)
|
||||
case .knocked: .knock
|
||||
default: nil
|
||||
@@ -263,7 +263,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
|
||||
|
||||
return RoomSummary(roomListItem: roomListItem,
|
||||
id: roomInfo.id,
|
||||
knockRequestType: knockRequestType,
|
||||
joinRequestType: joinRequestType,
|
||||
name: roomInfo.displayName ?? roomInfo.id,
|
||||
isDirect: roomInfo.isDirect,
|
||||
avatarURL: roomInfo.avatarUrl.flatMap(URL.init(string:)),
|
||||
|
||||
Reference in New Issue
Block a user