Fixes #2360 - Add support for manually marking rooms as unread (#2408)

* Fixes #2360 - Add support for manually marking rooms as unread

* Add analytics for marking rooms as (un)read

* Remove unnecessary analytics abstraction levels

* Fix breaking API changes following SDK bump

* Add changelog

* Handle PR comments

* Fix unit tests
This commit is contained in:
Stefan Ceriu
2024-02-05 18:06:50 +02:00
committed by GitHub
parent e80612b968
commit 9915a33964
46 changed files with 250 additions and 305 deletions
@@ -1,53 +0,0 @@
//
// Copyright 2023 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import AnalyticsEvents
enum AnalyticsMessageType {
case location(LocationType)
case poll
case text
case voiceMessage
enum LocationType {
case pin
case user
}
}
extension AnalyticsEvent.Composer.MessageType {
init(_ analyticsMessageType: AnalyticsMessageType) {
switch analyticsMessageType {
case .location(let locationType):
self = .init(locationType)
case .poll:
self = .Poll
case .text:
self = .Text
case .voiceMessage:
self = .VoiceMessage
}
}
private init(_ locationType: AnalyticsMessageType.LocationType) {
switch locationType {
case .pin:
self = .LocationPin
case .user:
self = .LocationUser
}
}
}
@@ -1,159 +0,0 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import AnalyticsEvents
import Foundation
enum AnalyticsScreen: Int {
case welcome
case login
case register
case forgotPassword
case sidebar
case home
case favourites
case people
case rooms
case searchRooms
case searchMessages
case searchPeople
case searchFiles
case room
case roomPreview
case roomDetails
case roomMembers
case user
case roomSearch
case roomUploads
case roomSettings
case roomNotifications
case roomDirectory
case switchDirectory
case startChat
case createRoom
case settings
case settingsSecurity
case settingsDefaultNotifications
case settingsMentionsAndKeywords
case settingsNotifications
case deactivateAccount
case inviteFriends
case threadList
case spaceMenu
case spaceMembers
case spaceExploreRooms
case dialpad
case spaceBottomSheet
case invites
case createSpace
case locationSend
case locationView
case createPoll
/// The screen name reported to the AnalyticsEvent.
var screenName: AnalyticsEvent.MobileScreen.ScreenName {
switch self {
case .welcome:
return .Welcome
case .login:
return .Login
case .register:
return .Register
case .forgotPassword:
return .ForgotPassword
case .sidebar:
return .Sidebar
case .home:
return .Home
case .favourites:
return .Favourites
case .people:
return .People
case .rooms:
return .Rooms
case .searchRooms:
return .SearchRooms
case .searchMessages:
return .SearchMessages
case .searchPeople:
return .SearchPeople
case .searchFiles:
return .SearchFiles
case .room:
return .Room
case .roomDetails:
return .RoomDetails
case .roomMembers:
return .RoomMembers
case .user:
return .User
case .roomPreview:
return .RoomPreview
case .roomSearch:
return .RoomSearch
case .roomUploads:
return .RoomUploads
case .roomSettings:
return .RoomSettings
case .roomNotifications:
return .RoomNotifications
case .roomDirectory:
return .RoomDirectory
case .switchDirectory:
return .SwitchDirectory
case .startChat:
return .StartChat
case .createRoom:
return .CreateRoom
case .settings:
return .Settings
case .settingsSecurity:
return .SettingsSecurity
case .settingsDefaultNotifications:
return .SettingsDefaultNotifications
case .settingsMentionsAndKeywords:
return .SettingsMentionsAndKeywords
case .settingsNotifications:
return .SettingsNotifications
case .deactivateAccount:
return .DeactivateAccount
case .inviteFriends:
return .InviteFriends
case .threadList:
return .ThreadList
case .spaceMenu:
return .SpaceMenu
case .spaceMembers:
return .SpaceMembers
case .spaceExploreRooms:
return .SpaceExploreRooms
case .dialpad:
return .Dialpad
case .spaceBottomSheet:
return .SpaceBottomSheet
case .invites:
return .Invites
case .createSpace:
return .CreateSpace
case .locationSend:
return .LocationSend
case .locationView:
return .LocationView
case .createPoll:
return .CreatePollView
}
}
}
@@ -121,11 +121,15 @@ extension AnalyticsService {
/// Track the presentation of a screen
/// - Parameter screen: The screen that was shown
/// - Parameter duration: An optional value representing how long the screen was shown for in milliseconds.
func track(screen: AnalyticsScreen, duration milliseconds: Int? = nil) {
func track(screen: AnalyticsEvent.MobileScreen.ScreenName, duration milliseconds: Int? = nil) {
MXLog.debug("\(screen)")
let event = AnalyticsEvent.MobileScreen(durationMs: milliseconds, screenName: screen.screenName)
let event = AnalyticsEvent.MobileScreen(durationMs: milliseconds, screenName: screen)
client.screen(event)
}
func trackInteraction(index: Int? = nil, name: AnalyticsEvent.Interaction.Name) {
capture(event: AnalyticsEvent.Interaction(index: index, interactionType: .Touch, name: name))
}
/// Track the creation of a room
/// - Parameter isDM: true if the created room is a direct message, false otherwise
@@ -143,12 +147,12 @@ extension AnalyticsService {
func trackComposer(inThread: Bool,
isEditing: Bool,
isReply: Bool,
messageType: AnalyticsMessageType = .text,
messageType: AnalyticsEvent.Composer.MessageType = .Text,
startsThread: Bool?) {
capture(event: AnalyticsEvent.Composer(inThread: inThread,
isEditing: isEditing,
isReply: isReply,
messageType: .init(messageType),
messageType: messageType,
startsThread: startsThread))
}
@@ -587,20 +587,20 @@ class ClientProxy: ClientProxyProtocol {
}
private let eventFilters: TimelineEventTypeFilter = {
let stateEventFilters: [FilterStateEventType] = [.roomAliases,
.roomCanonicalAlias,
.roomGuestAccess,
.roomHistoryVisibility,
.roomJoinRules,
.roomPinnedEvents,
.roomPowerLevels,
.roomServerAcl,
.roomTombstone,
.spaceChild,
.spaceParent,
.policyRuleRoom,
.policyRuleServer,
.policyRuleUser]
let stateEventFilters: [StateEventType] = [.roomAliases,
.roomCanonicalAlias,
.roomGuestAccess,
.roomHistoryVisibility,
.roomJoinRules,
.roomPinnedEvents,
.roomPowerLevels,
.roomServerAcl,
.roomTombstone,
.spaceChild,
.spaceParent,
.policyRuleRoom,
.policyRuleServer,
.policyRuleUser]
return .exclude(eventTypes: stateEventFilters.map { FilterTimelineEventType.state(eventType: $0) })
}()
+44 -13
View File
@@ -45,18 +45,24 @@ class RoomProxy: RoomProxyProtocol {
var actions: AnyPublisher<RoomProxyAction, Never> {
actionsSubject.eraseToAnyPublisher()
}
var ownUserID: String {
room.ownUserId()
}
init(roomListItem: RoomListItemProtocol,
room: RoomProtocol,
backgroundTaskService: BackgroundTaskServiceProtocol) async {
init?(roomListItem: RoomListItemProtocol,
room: RoomProtocol,
backgroundTaskService: BackgroundTaskServiceProtocol) async {
self.roomListItem = roomListItem
self.room = room
self.backgroundTaskService = backgroundTaskService
timeline = await TimelineProxy(timeline: room.timeline(), backgroundTaskService: backgroundTaskService)
do {
timeline = try await TimelineProxy(timeline: room.timeline(), backgroundTaskService: backgroundTaskService)
} catch {
MXLog.error("Failed creating timeline with error: \(error)")
return nil
}
Task {
await updateMembers()
@@ -124,11 +130,7 @@ class RoomProxy: RoomProxyProtocol {
var canonicalAlias: String? {
room.canonicalAlias()
}
var hasUnreadNotifications: Bool {
roomListItem.hasUnreadNotifications()
}
var avatarURL: URL? {
roomListItem.avatarUrl().flatMap(URL.init(string:))
}
@@ -352,6 +354,37 @@ class RoomProxy: RoomProxyProtocol {
}
}
func markAsUnread() async -> Result<Void, RoomProxyError> {
MXLog.info("Marking room \(id) as unread")
do {
try await room.markAsUnread()
return .success(())
} catch {
MXLog.error("Failed marking room \(id) as unread with error: \(error)")
return .failure(.failedMarkingAsUnread)
}
}
func markAsRead(sendReadReceipts: Bool, receiptType: ReceiptType) async -> Result<Void, RoomProxyError> {
MXLog.info("Marking room \(id) as read, sending read receipts: \(sendReadReceipts)")
do {
if sendReadReceipts {
try await room.markAsReadAndSendReadReceipt(receiptType: receiptType)
} else {
try await room.markAsRead()
}
return .success(())
} catch {
MXLog.error("Failed marking room \(id) as read with error: \(error)")
return .failure(.failedMarkingAsRead)
}
}
// MARK: - Element Call
func canUserJoinCall(userID: String) async -> Result<Bool, RoomProxyError> {
do {
return try await .success(room.canUserSendState(userId: userID, stateEvent: .callMember))
@@ -361,8 +394,6 @@ class RoomProxy: RoomProxyProtocol {
}
}
// MARK: - Element Call
func elementCallWidgetDriver() -> ElementCallWidgetDriverProtocol {
ElementCallWidgetDriver(room: room)
}
@@ -31,6 +31,8 @@ enum RoomProxyError: Error, Equatable {
case failedRemovingAvatar
case failedUploadingAvatar
case failedCheckingPermission
case failedMarkingAsRead
case failedMarkingAsUnread
}
enum RoomProxyAction {
@@ -47,7 +49,6 @@ protocol RoomProxyProtocol {
var membership: Membership { get }
var hasOngoingCall: Bool { get }
var canonicalAlias: String? { get }
var hasUnreadNotifications: Bool { get }
var ownUserID: String { get }
var name: String? { get }
@@ -103,10 +104,14 @@ protocol RoomProxyProtocol {
func canUserTriggerRoomNotification(userID: String) async -> Result<Bool, RoomProxyError>
func canUserJoinCall(userID: String) async -> Result<Bool, RoomProxyError>
func markAsUnread() async -> Result<Void, RoomProxyError>
func markAsRead(sendReadReceipts: Bool, receiptType: ReceiptType) async -> Result<Void, RoomProxyError>
// MARK: - Element Call
func canUserJoinCall(userID: String) async -> Result<Bool, RoomProxyError>
func elementCallWidgetDriver() -> ElementCallWidgetDriverProtocol
}
@@ -83,6 +83,7 @@ extension Array where Element == RoomSummary {
avatarURL: nil,
lastMessage: AttributedString("I do not wish to take the trouble to understand mysticism"),
lastMessageFormattedTimestamp: "14:56",
isMarkedUnread: false,
unreadMessagesCount: 0,
unreadMentionsCount: 0,
unreadNotificationsCount: 0,
@@ -96,6 +97,7 @@ extension Array where Element == RoomSummary {
avatarURL: URL.picturesDirectory,
lastMessage: AttributedString("How do you see the Emperor then? You think he keeps office hours?"),
lastMessageFormattedTimestamp: "2:56 PM",
isMarkedUnread: false,
unreadMessagesCount: 2,
unreadMentionsCount: 0,
unreadNotificationsCount: 2,
@@ -109,6 +111,7 @@ extension Array where Element == RoomSummary {
avatarURL: nil,
lastMessage: try? AttributedString(markdown: "He certainly seemed no *mental genius* to me"),
lastMessageFormattedTimestamp: "Some time ago",
isMarkedUnread: false,
unreadMessagesCount: 3,
unreadMentionsCount: 0,
unreadNotificationsCount: 0,
@@ -122,6 +125,7 @@ extension Array where Element == RoomSummary {
avatarURL: nil,
lastMessage: AttributedString("There's an archaic measure of time that's called the month"),
lastMessageFormattedTimestamp: "Just now",
isMarkedUnread: false,
unreadMessagesCount: 2,
unreadMentionsCount: 2,
unreadNotificationsCount: 2,
@@ -135,6 +139,7 @@ extension Array where Element == RoomSummary {
avatarURL: nil,
lastMessage: AttributedString("Clearly, if Earth is powerful enough to do that, it might also be capable of adjusting minds in order to force belief in its radioactivity"),
lastMessageFormattedTimestamp: "1986",
isMarkedUnread: false,
unreadMessagesCount: 1,
unreadMentionsCount: 1,
unreadNotificationsCount: 1,
@@ -148,6 +153,7 @@ extension Array where Element == RoomSummary {
avatarURL: nil,
lastMessage: AttributedString("Are you groping for the word 'paranoia'?"),
lastMessageFormattedTimestamp: "きょうはじゅういちがつじゅういちにちです",
isMarkedUnread: false,
unreadMessagesCount: 6,
unreadMentionsCount: 0,
unreadNotificationsCount: 0,
@@ -161,6 +167,7 @@ extension Array where Element == RoomSummary {
avatarURL: nil,
lastMessage: nil,
lastMessageFormattedTimestamp: nil,
isMarkedUnread: false,
unreadMessagesCount: 0,
unreadMentionsCount: 0,
unreadNotificationsCount: 0,
@@ -206,6 +213,7 @@ extension Array where Element == RoomSummary {
avatarURL: URL.picturesDirectory,
lastMessage: nil,
lastMessageFormattedTimestamp: nil,
isMarkedUnread: false,
unreadMessagesCount: 0,
unreadMentionsCount: 0,
unreadNotificationsCount: 0,
@@ -219,6 +227,7 @@ extension Array where Element == RoomSummary {
avatarURL: nil,
lastMessage: nil,
lastMessageFormattedTimestamp: nil,
isMarkedUnread: false,
unreadMessagesCount: 0,
unreadMentionsCount: 0,
unreadNotificationsCount: 0,
@@ -24,6 +24,7 @@ struct RoomSummaryDetails {
let avatarURL: URL?
let lastMessage: AttributedString?
let lastMessageFormattedTimestamp: String?
let isMarkedUnread: Bool
let unreadMessagesCount: UInt
let unreadMentionsCount: UInt
let unreadNotificationsCount: UInt
@@ -54,6 +55,7 @@ extension RoomSummaryDetails {
avatarURL = nil
lastMessage = AttributedString(string)
lastMessageFormattedTimestamp = "Now"
isMarkedUnread = false
unreadMessagesCount = hasUnreadMessages ? 1 : 0
unreadMentionsCount = hasUnreadMentions ? 1 : 0
unreadNotificationsCount = hasUnreadNotifications ? 1 : 0
@@ -248,6 +248,7 @@ class RoomSummaryProvider: RoomSummaryProviderProtocol {
avatarURL: roomInfo.avatarUrl.flatMap(URL.init(string:)),
lastMessage: attributedLastMessage,
lastMessageFormattedTimestamp: lastMessageFormattedTimestamp,
isMarkedUnread: appSettings.markAsUnreadEnabled ? roomInfo.isMarkedUnread : false,
unreadMessagesCount: appSettings.mentionsBadgeEnabled ? UInt(roomInfo.numUnreadMessages) : 0,
unreadMentionsCount: appSettings.mentionsBadgeEnabled ? UInt(roomInfo.numUnreadMentions) : 0,
unreadNotificationsCount: appSettings.mentionsBadgeEnabled ? UInt(roomInfo.numUnreadNotifications) : UInt(roomInfo.notificationCount),
@@ -92,8 +92,7 @@ class RoomTimelineController: RoomTimelineControllerProtocol {
}
func sendReadReceipt(for itemID: TimelineItemIdentifier) async -> Result<Void, RoomTimelineControllerError> {
guard roomProxy.hasUnreadNotifications,
let eventID = itemID.eventID
guard let eventID = itemID.eventID
else { return .success(()) }
switch await roomProxy.timeline.sendReadReceipt(for: eventID,