From 919e1d6adb996ca0d5270676a04eab928a28ac52 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Wed, 3 Nov 2021 13:36:18 +0300 Subject: [PATCH 01/87] Add context menu item --- .../Room/ContextMenu/Contents.json | 6 ++--- .../Contents.json | 23 ++++++++++++++++++ .../Thread.png | Bin 0 -> 401 bytes .../Thread@2x.png | Bin 0 -> 637 bytes .../Thread@3x.png | Bin 0 -> 945 bytes Riot/Assets/en.lproj/Vector.strings | 1 + Riot/Generated/Images.swift | 1 + Riot/Generated/Strings.swift | 4 +++ .../RoomContextualMenuAction.swift | 5 ++++ Riot/Modules/Room/RoomViewController.m | 19 +++++++++++++++ 10 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_reply_in_thread.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_reply_in_thread.imageset/Thread.png create mode 100644 Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_reply_in_thread.imageset/Thread@2x.png create mode 100644 Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_reply_in_thread.imageset/Thread@3x.png diff --git a/Riot/Assets/Images.xcassets/Room/ContextMenu/Contents.json b/Riot/Assets/Images.xcassets/Room/ContextMenu/Contents.json index da4a164c9..73c00596a 100644 --- a/Riot/Assets/Images.xcassets/Room/ContextMenu/Contents.json +++ b/Riot/Assets/Images.xcassets/Room/ContextMenu/Contents.json @@ -1,6 +1,6 @@ { "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_reply_in_thread.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_reply_in_thread.imageset/Contents.json new file mode 100644 index 000000000..d2c033d2d --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_reply_in_thread.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "Thread.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Thread@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Thread@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_reply_in_thread.imageset/Thread.png b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_reply_in_thread.imageset/Thread.png new file mode 100644 index 0000000000000000000000000000000000000000..28f8fa589f5243d365bdcd815a9f1b2199bf712e GIT binary patch literal 401 zcmV;C0dD?@P)$l~R;6LpCIAH_4UBO^e*qA<8o3b#z&jr*AV0mW0WN5(dr=U3$a z@8?%4XlcsDwLG{xZY1n61q*!k=DGZ2=m2Tql}VkWjHDQgZStkqa>NeonY}+ib%x;S zeTakeU?W5thwVm{McaXZ%~|KG?VzWr$dqH;fqvdq81KMjHqG^32`IWiPbu|wrUV3i z5q`x9=bx}fVtyY5=>}IH`QAjv0C%!_xfxK#Z%sdo@Q1J4cum63Mj{NdBi_vFF)-)| zERpbb>A;mb;5yy`+js|T6IRrRqs!+#l^YWEw*gCqMu5l{)jdU2p8+BcOE+GF(ci#S zqOt=7xB}xReN*LxStAByoAh)q8CAU~$@py z$OBkweK%z-d7#evZpzB@K;8A-6rTrd;~lV#cfdAIJD?Al%XFM|Kp*d?LSD@XiH2Fd z%6T9@e`*E{b;tS$a%HU1tE6sj(k`S3M#w7_Tu7 zVqF$9HJ#6Qot14)>R{x+B#64V_AWc-k-uUZZ$M@QuZx1nHU%@FcU(GAVZ+5w#^V~l Xq+-)^4@P9(00000NkvXXu0mjf$@UNq literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_reply_in_thread.imageset/Thread@3x.png b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_reply_in_thread.imageset/Thread@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..b7400c74bbee2b09a697274eccb04201792a2da1 GIT binary patch literal 945 zcmV;i15W&jP)2PGvCL%uWxL`!@Cd-E0H`up zgiz2|LiIt3tIp}uPYbVGWYTllEsvwyF@NEf- z#n6f3z4qH@{j^D`0?jPvI}}fxTZW$N?xtCileT&sjf-eGe^Z);4lP=N>aT(WG_W!h zpGqA!AKjp*U#AYV9vG|F(bgyor5u6<_)?af=+2=4(M=jx)14W5Nn=`pgd^@LmGFJ6 zs=-+!1dEv6$Pg{11deFUj|*@ry0uytLvePJz8{agOR$t^%hTLg{WD!^kWL~Dt5&#G z5}{oF+Lsn+(_n!JSReuxh=2tmV1Wo&AOaSMzypKSj)N9f?tEBZ1z?Ez)@5)~uYw_r zSx~Lj>7qCi)>|GaDNug3*_L1kb1oLEL&ymZh$Aor#~=b@0$~~C0`h`_4`2vm0(H(B z5-;omOGs>bN{N`c?^QkQ4E*JYB`BWA-7bD;pM8Ex4KJL8jLJz=SO{0!KW0RrfBVT4*aB8Be{|l|u5{y>U95f_9L-v$DqtCS8-K;_ct35<$`<6y zZpl$`3u`x4mN%y2IPrccUkuXbtt8NmOHF#oL>cS@y$X7WJE;Y~UE>=*Nnn!Cv)7>axD=)fB z&7-xTq@&~;m%85F6mcfFx`A(BKlkoGc(|)cell { MXWeakify(self); From 01f7a7b243142dca8483ba5fddb32e565de8dc8a Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Thu, 4 Nov 2021 02:07:09 +0300 Subject: [PATCH 02/87] Adapt to send event api changes --- Riot/Modules/Application/LegacyAppDelegate.m | 2 +- RiotShareExtension/Shared/ShareManager.m | 10 +++++----- .../Service/MatrixSDK/TemplateRoomChatService.swift | 2 +- SiriIntents/IntentHandler.m | 2 ++ 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index 6b9816643..81cbc7ad4 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -3405,7 +3405,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni @"party_id": mxSession.myDeviceId }; - [mxSession.matrixRestClient sendEventToRoom:event.roomId eventType:kMXEventTypeStringCallReject content:content txnId:nil success:nil failure:^(NSError *error) { + [mxSession.matrixRestClient sendEventToRoom:event.roomId threadId:nil eventType:kMXEventTypeStringCallReject content:content txnId:nil success:nil failure:^(NSError *error) { MXLogDebug(@"[AppDelegate] enableNoVoIPOnMatrixSession: ERROR: Cannot send m.call.reject event."); }]; diff --git a/RiotShareExtension/Shared/ShareManager.m b/RiotShareExtension/Shared/ShareManager.m index fdec722ed..9049e2eff 100644 --- a/RiotShareExtension/Shared/ShareManager.m +++ b/RiotShareExtension/Shared/ShareManager.m @@ -724,7 +724,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) dispatch_group_t dispatchGroup = dispatch_group_create(); for (MXRoom *room in rooms) { dispatch_group_enter(dispatchGroup); - [room sendTextMessage:text success:^(NSString *eventId) { + [room sendTextMessage:text threadId:nil success:^(NSString *eventId) { dispatch_group_leave(dispatchGroup); } failure:^(NSError *innerError) { MXLogError(@"[ShareManager] sendTextMessage failed with error %@", error); @@ -764,7 +764,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) dispatch_group_t dispatchGroup = dispatch_group_create(); for (MXRoom *room in rooms) { dispatch_group_enter(dispatchGroup); - [room sendFile:fileUrl mimeType:mimeType localEcho:nil success:^(NSString *eventId) { + [room sendFile:fileUrl mimeType:mimeType threadId:nil localEcho:nil success:^(NSString *eventId) { dispatch_group_leave(dispatchGroup); } failure:^(NSError *innerError) { MXLogError(@"[ShareManager] sendFile failed with error %@", innerError); @@ -815,7 +815,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) dispatch_group_t dispatchGroup = dispatch_group_create(); for (MXRoom *room in rooms) { dispatch_group_enter(dispatchGroup); - [room sendVideoAsset:videoAsset withThumbnail:videoThumbnail localEcho:nil success:^(NSString *eventId) { + [room sendVideoAsset:videoAsset withThumbnail:videoThumbnail threadId:nil localEcho:nil success:^(NSString *eventId) { dispatch_group_leave(dispatchGroup); } failure:^(NSError *innerError) { MXLogError(@"[ShareManager] Failed sending video with error %@", innerError); @@ -901,7 +901,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) dispatch_group_t dispatchGroup = dispatch_group_create(); for (MXRoom *room in rooms) { dispatch_group_enter(dispatchGroup); - [room sendVoiceMessage:fileUrl mimeType:nil duration:0.0 samples:nil localEcho:nil success:^(NSString *eventId) { + [room sendVoiceMessage:fileUrl mimeType:nil duration:0.0 samples:nil threadId:nil localEcho:nil success:^(NSString *eventId) { dispatch_group_leave(dispatchGroup); } failure:^(NSError *innerError) { MXLogError(@"[ShareManager] sendVoiceMessage failed with error %@", error); @@ -1066,7 +1066,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) } dispatch_group_enter(dispatchGroup); - [room sendImage:finalImageData withImageSize:imageSize mimeType:mimeType andThumbnail:thumbnail localEcho:nil success:^(NSString *eventId) { + [room sendImage:finalImageData withImageSize:imageSize mimeType:mimeType andThumbnail:thumbnail threadId:nil localEcho:nil success:^(NSString *eventId) { dispatch_group_leave(dispatchGroup); } failure:^(NSError *innerError) { MXLogError(@"[ShareManager] sendImage failed with error %@", error); diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/MatrixSDK/TemplateRoomChatService.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/MatrixSDK/TemplateRoomChatService.swift index a749cde50..07d7dc615 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/MatrixSDK/TemplateRoomChatService.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/Service/MatrixSDK/TemplateRoomChatService.swift @@ -57,7 +57,7 @@ class TemplateRoomChatService: TemplateRoomChatServiceProtocol { // MARK: Public func send(textMessage: String) { var localEcho: MXEvent? = nil - room.sendTextMessage(textMessage, localEcho: &localEcho, completion: { _ in }) + room.sendTextMessage(textMessage, threadId: nil, localEcho: &localEcho, completion: { _ in }) } // MARK: Private diff --git a/SiriIntents/IntentHandler.m b/SiriIntents/IntentHandler.m index c55a5357e..5ba8a7a67 100644 --- a/SiriIntents/IntentHandler.m +++ b/SiriIntents/IntentHandler.m @@ -245,6 +245,7 @@ session.crypto.warnOnUnknowDevices = NO; [room sendTextMessage:intent.content + threadId:nil success:^(NSString *eventId) { completeWithCode(INSendMessageIntentResponseCodeSuccess); } failure:^(NSError *error) { @@ -259,6 +260,7 @@ } [account.mxRestClient sendTextMessageToRoom:roomID + threadId:nil text:intent.content success:^(NSString *eventId) { completeWithCode(INSendMessageIntentResponseCodeSuccess); From 048c982a1ca51e7c28b135e9c91766f43696dfc7 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Thu, 4 Nov 2021 02:07:50 +0300 Subject: [PATCH 03/87] Initialize thread data source --- .../Room/DataSources/ThreadDataSource.swift | 24 +++++++ Riot/Modules/Room/RoomCoordinator.swift | 42 ++++++++++-- .../Room/RoomCoordinatorBridgePresenter.swift | 11 ++- .../Room/RoomCoordinatorParameters.swift | 12 +++- Riot/Modules/Room/RoomViewController.m | 67 +++++++++++++------ .../Room/Search/RoomSearchViewController.m | 4 +- 6 files changed, 131 insertions(+), 29 deletions(-) create mode 100644 Riot/Modules/Room/DataSources/ThreadDataSource.swift diff --git a/Riot/Modules/Room/DataSources/ThreadDataSource.swift b/Riot/Modules/Room/DataSources/ThreadDataSource.swift new file mode 100644 index 000000000..6f67c1375 --- /dev/null +++ b/Riot/Modules/Room/DataSources/ThreadDataSource.swift @@ -0,0 +1,24 @@ +// +// 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 Foundation + +@objcMembers +public class ThreadDataSource: RoomDataSource { + + + +} diff --git a/Riot/Modules/Room/RoomCoordinator.swift b/Riot/Modules/Room/RoomCoordinator.swift index 4764e03e9..03ad0b977 100644 --- a/Riot/Modules/Room/RoomCoordinator.swift +++ b/Riot/Modules/Room/RoomCoordinator.swift @@ -97,8 +97,10 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { // FIXME: Find a better way to manage modal dismiss. This makes the `roomViewController` to never be released // self.roomViewController.presentationController?.delegate = self - if let eventId = self.selectedEventId { - self.loadRoom(withId: self.parameters.roomId, and: eventId, completion: completion) + if let threadId = self.parameters.threadId { + self.loadRoom(withId: self.parameters.roomId, andThreadId: threadId, completion: completion) + } else if let eventId = self.selectedEventId { + self.loadRoom(withId: self.parameters.roomId, andEventId: eventId, completion: completion) } else { self.loadRoom(withId: self.parameters.roomId, completion: completion) } @@ -118,7 +120,7 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { self.selectedEventId = eventId if self.hasStartedOnce { - self.loadRoom(withId: self.parameters.roomId, and: eventId, completion: completion) + self.loadRoom(withId: self.parameters.roomId, andEventId: eventId, completion: completion) } else { self.start(withCompletion: completion) } @@ -154,7 +156,7 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { }) } - private func loadRoom(withId roomId: String, and eventId: String, completion: (() -> Void)?) { + private func loadRoom(withId roomId: String, andEventId eventId: String, completion: (() -> Void)?) { // Present activity indicator when retrieving roomDataSource for given room ID self.activityIndicatorPresenter.presentActivityIndicator(on: roomViewController.view, animated: false) @@ -162,6 +164,7 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { // Open the room on the requested event RoomDataSource.load(withRoomId: roomId, initialEventId: eventId, + threadId: nil, andMatrixSession: self.parameters.session) { [weak self] (dataSource) in guard let self = self else { @@ -183,6 +186,37 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { completion?() } } + + private func loadRoom(withId roomId: String, andThreadId threadId: String, completion: (() -> Void)?) { + + // Present activity indicator when retrieving roomDataSource for given room ID + self.activityIndicatorPresenter.presentActivityIndicator(on: roomViewController.view, animated: false) + + // Open the room on the requested event + ThreadDataSource.load(withRoomId: roomId, + initialEventId: nil, + threadId: threadId, + andMatrixSession: self.parameters.session) { [weak self] (dataSource) in + + guard let self = self else { + return + } + + self.activityIndicatorPresenter.removeCurrentActivityIndicator(animated: true) + + guard let threadDataSource = dataSource as? ThreadDataSource else { + return + } + + threadDataSource.markTimelineInitialEvent = false + self.roomViewController.displayRoom(threadDataSource) + + // Give the data source ownership to the room view controller. + self.roomViewController.hasRoomDataSourceOwnership = true + + completion?() + } + } } // MARK: - RoomIdentifiable diff --git a/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift b/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift index d6ec2aa4e..7960dee18 100644 --- a/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift +++ b/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift @@ -34,16 +34,21 @@ class RoomCoordinatorBridgePresenterParameters: NSObject { /// If not nil, the room will be opened on this event. let eventId: String? + /// If not nil, specified thread will be opened. + let threadId: String? + /// The data for the room preview. let previewData: RoomPreviewData? init(session: MXSession, roomId: String, eventId: String?, + threadId: String?, previewData: RoomPreviewData?) { self.session = session self.roomId = roomId self.eventId = eventId + self.threadId = threadId self.previewData = previewData } } @@ -117,7 +122,11 @@ final class RoomCoordinatorBridgePresenter: NSObject { if let previewData = self.bridgeParameters.previewData { coordinatorParameters = RoomCoordinatorParameters(navigationRouter: navigationRouter, previewData: previewData) } else { - coordinatorParameters = RoomCoordinatorParameters(navigationRouter: navigationRouter, session: self.bridgeParameters.session, roomId: self.bridgeParameters.roomId, eventId: self.bridgeParameters.eventId) + coordinatorParameters = RoomCoordinatorParameters(navigationRouter: navigationRouter, + session: self.bridgeParameters.session, + roomId: self.bridgeParameters.roomId, + eventId: self.bridgeParameters.eventId, + threadId: self.bridgeParameters.threadId) } return RoomCoordinator(parameters: coordinatorParameters) diff --git a/Riot/Modules/Room/RoomCoordinatorParameters.swift b/Riot/Modules/Room/RoomCoordinatorParameters.swift index fbc9f3511..1ed73f8cf 100644 --- a/Riot/Modules/Room/RoomCoordinatorParameters.swift +++ b/Riot/Modules/Room/RoomCoordinatorParameters.swift @@ -37,6 +37,9 @@ struct RoomCoordinatorParameters { /// If not nil, the room will be opened on this event. let eventId: String? + /// If not nil, specified thread will be opened. + let threadId: String? + /// The data for the room preview. let previewData: RoomPreviewData? @@ -47,12 +50,14 @@ struct RoomCoordinatorParameters { session: MXSession, roomId: String, eventId: String?, + threadId: String?, previewData: RoomPreviewData?) { self.navigationRouter = navigationRouter self.navigationRouterStore = navigationRouterStore self.session = session self.roomId = roomId self.eventId = eventId + self.threadId = threadId self.previewData = previewData } @@ -61,9 +66,10 @@ struct RoomCoordinatorParameters { navigationRouterStore: NavigationRouterStoreProtocol? = nil, session: MXSession, roomId: String, - eventId: String? = nil) { + eventId: String? = nil, + threadId: String? = nil) { - self.init(navigationRouter: navigationRouter, navigationRouterStore: navigationRouterStore, session: session, roomId: roomId, eventId: eventId, previewData: nil) + self.init(navigationRouter: navigationRouter, navigationRouterStore: navigationRouterStore, session: session, roomId: roomId, eventId: eventId, threadId: threadId, previewData: nil) } /// Init to present a room preview @@ -71,6 +77,6 @@ struct RoomCoordinatorParameters { navigationRouterStore: NavigationRouterStoreProtocol? = nil, previewData: RoomPreviewData) { - self.init(navigationRouter: navigationRouter, navigationRouterStore: navigationRouterStore, session: previewData.mxSession, roomId: previewData.roomId, eventId: nil, previewData: previewData) + self.init(navigationRouter: navigationRouter, navigationRouterStore: navigationRouterStore, session: previewData.mxSession, roomId: previewData.roomId, eventId: nil, threadId: nil, previewData: previewData) } } diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 6a6ca85cc..07c1b6859 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -244,6 +244,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; @property (nonatomic, strong) RoomMessageURLParser *roomMessageURLParser; @property (nonatomic, strong) RoomCreationModalCoordinatorBridgePresenter *roomCreationModalCoordinatorBridgePresenter; @property (nonatomic, strong) RoomInfoCoordinatorBridgePresenter *roomInfoCoordinatorBridgePresenter; +@property (nonatomic, strong) RoomCoordinatorBridgePresenter *threadBridgePresenter; @property (nonatomic, strong) CustomSizedPresentationController *customSizedPresentationController; @property (nonatomic, getter=isActivitiesViewExpanded) BOOL activitiesViewExpanded; @property (nonatomic, getter=isScrollToBottomHidden) BOOL scrollToBottomHidden; @@ -4364,7 +4365,11 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; // Jump to the last unread event by using a temporary room data source initialized with the last unread event id. MXWeakify(self); - [RoomDataSource loadRoomDataSourceWithRoomId:self.roomDataSource.roomId initialEventId:self.roomDataSource.room.accountData.readMarkerEventId andMatrixSession:self.mainSession onComplete:^(id roomDataSource) { + [RoomDataSource loadRoomDataSourceWithRoomId:self.roomDataSource.roomId + initialEventId:self.roomDataSource.room.accountData.readMarkerEventId + threadId:self.roomDataSource.threadId + andMatrixSession:self.mainSession + onComplete:^(id roomDataSource) { MXStrongifyAndReturnIfNil(self); [roomDataSource finalizeInitialization]; @@ -4587,7 +4592,11 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; if (eventId) { MXWeakify(self); - [RoomDataSource loadRoomDataSourceWithRoomId:self.roomDataSource.roomId initialEventId:eventId andMatrixSession:self.mainSession onComplete:^(id roomDataSource) { + [RoomDataSource loadRoomDataSourceWithRoomId:self.roomDataSource.roomId + initialEventId:eventId + threadId:self.roomDataSource.threadId + andMatrixSession:self.mainSession + onComplete:^(id roomDataSource) { MXStrongifyAndReturnIfNil(self); [roomDataSource finalizeInitialization]; @@ -5915,27 +5924,25 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; ]; } - BOOL showMoreOption = (event.isState && RiotSettings.shared.roomContextualMenuShowMoreOptionForStates) || (!event.isState && RiotSettings.shared.roomContextualMenuShowMoreOptionForMessages); + BOOL showMoreOption = (event.isState && RiotSettings.shared.roomContextualMenuShowMoreOptionForStates) + || (!event.isState && RiotSettings.shared.roomContextualMenuShowMoreOptionForMessages); + NSMutableArray *items = [NSMutableArray arrayWithCapacity:5]; + + [items addObject:[self copyMenuItemWithEvent:event andCell:cell]]; + [items addObject:[self replyMenuItemWithEvent:event]]; + if (!event.threadIdentifier) + { + // add "reply in thread" option only if not a thread already + [items addObject:[self replyInThreadMenuItemWithEvent:event]]; + } + [items addObject:[self editMenuItemWithEvent:event]]; if (showMoreOption) { - return @[ - [self copyMenuItemWithEvent:event andCell:cell], - [self replyMenuItemWithEvent:event], - [self replyInThreadMenuItemWithEvent:event], - [self editMenuItemWithEvent:event], - [self moreMenuItemWithEvent:event andCell:cell] - ]; - } - else - { - return @[ - [self copyMenuItemWithEvent:event andCell:cell], - [self replyMenuItemWithEvent:event], - [self replyInThreadMenuItemWithEvent:event], - [self editMenuItemWithEvent:event] - ]; + [items addObject:[self moreMenuItemWithEvent:event andCell:cell]]; } + + return items; } - (void)showContextualMenuForEvent:(MXEvent*)event fromSingleTapGesture:(BOOL)usedSingleTapGesture cell:(id)cell animated:(BOOL)animated @@ -6245,7 +6252,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [self hideContextualMenuAnimated:YES cancelEventSelection:NO completion:nil]; - // TODO: Implement starting a thread + [self openThreadWithId:event.eventId]; }; return item; @@ -6265,6 +6272,26 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; return moreMenuItem; } +#pragma mark - Threads + +- (void)openThreadWithId:(NSString *)threadId +{ + if (self.threadBridgePresenter) + { + [self.threadBridgePresenter dismissWithAnimated:YES completion:nil]; + self.threadBridgePresenter = nil; + } + + RoomCoordinatorBridgePresenterParameters *parameters = [[RoomCoordinatorBridgePresenterParameters alloc] initWithSession:self.mainSession + roomId:self.roomDataSource.roomId + eventId:nil + threadId:threadId + previewData:nil]; + RoomCoordinatorBridgePresenter *presenter = [[RoomCoordinatorBridgePresenter alloc] initWithParameters:parameters]; + self.threadBridgePresenter = presenter; + [presenter presentFrom:self animated:YES]; +} + #pragma mark - RoomContextualMenuViewControllerDelegate - (void)roomContextualMenuViewControllerDidTapBackgroundOverlay:(RoomContextualMenuViewController *)viewController diff --git a/Riot/Modules/Room/Search/RoomSearchViewController.m b/Riot/Modules/Room/Search/RoomSearchViewController.m index 08374baea..301427fbc 100644 --- a/Riot/Modules/Room/Search/RoomSearchViewController.m +++ b/Riot/Modules/Room/Search/RoomSearchViewController.m @@ -317,7 +317,9 @@ [RoomDataSource loadRoomDataSourceWithRoomId:selectedSearchEvent.roomId initialEventId:selectedSearchEvent.eventId - andMatrixSession:selectedSearchEventSession onComplete:^(RoomDataSource *roomDataSource) { + threadId:selectedSearchEvent.threadIdentifier + andMatrixSession:selectedSearchEventSession + onComplete:^(RoomDataSource *roomDataSource) { [roomDataSource finalizeInitialization]; roomDataSource.markTimelineInitialEvent = YES; From afaffce552964fa0833cf4beb20e5f63fed37495 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Tue, 9 Nov 2021 14:44:27 +0300 Subject: [PATCH 04/87] Introduce lab setting for threads --- Riot/Managers/Settings/RiotSettings.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift index 9e1ccd05b..50b330f87 100644 --- a/Riot/Managers/Settings/RiotSettings.swift +++ b/Riot/Managers/Settings/RiotSettings.swift @@ -117,6 +117,10 @@ final class RiotSettings: NSObject { @UserDefault(key: "enableRingingForGroupCalls", defaultValue: false, storage: defaults) var enableRingingForGroupCalls + /// Indicates if threads enabled in the timeline. + @UserDefault(key: "enableThreads", defaultValue: true, storage: defaults) + var enableThreads + // MARK: Calls /// Indicate if `allowStunServerFallback` settings has been set once. From 8be3d291916212370a5d0cfa577a0b67c3e6ed25 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Tue, 9 Nov 2021 14:46:35 +0300 Subject: [PATCH 05/87] Add additional height in the cell for thread summary view --- .../Room/CellData/RoomBubbleCellData.m | 41 +++++++++++++++++++ .../Encryption/RoomBubbleCellLayout.swift | 4 ++ 2 files changed, 45 insertions(+) diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.m b/Riot/Modules/Room/CellData/RoomBubbleCellData.m index a71e6d72e..72a7ae53b 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.m +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.m @@ -263,6 +263,17 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat return [super hasNoDisplay]; } +- (BOOL)hasThreadRoot +{ + if (!RiotSettings.shared.enableThreads) + { + // do not consider this cell data if threads not enabled in the timeline + return NO; + } + + return super.hasThreadRoot; +} + #pragma mark - Bubble collapsing - (BOOL)collapseWith:(id)cellData @@ -531,6 +542,8 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat additionalVerticalHeight+= [self urlPreviewHeightForEventId:eventId]; // Add vertical whitespace in case of reactions. additionalVerticalHeight+= [self reactionHeightForEventId:eventId]; + // Add vertical whitespace in case of a thread root + additionalVerticalHeight+= [self threadSummaryViewHeightForEventId:eventId]; // Add vertical whitespace in case of read receipts. additionalVerticalHeight+= [self readReceiptHeightForEventId:eventId]; @@ -550,6 +563,7 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat height+= [self urlPreviewHeightForEventId:eventId]; height+= [self reactionHeightForEventId:eventId]; + height+= [self threadSummaryViewHeightForEventId:eventId]; height+= [self readReceiptHeightForEventId:eventId]; } @@ -588,6 +602,33 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat self.shouldUpdateAdditionalContentHeight = YES; } +- (CGFloat)threadSummaryViewHeightForEventId:(NSString*)eventId +{ + if (!RiotSettings.shared.enableThreads) + { + // do not show thread summary view if threads not enabled in the timeline + return 0; + } + if (roomDataSource.threadId) + { + // do not show thread summary view on threads + return 0; + } + NSInteger index = [self bubbleComponentIndexForEventId:eventId]; + if (index == NSNotFound) + { + return 0; + } + MXKRoomBubbleComponent *component = self.bubbleComponents[index]; + if (!component.thread) + { + // component is not a thread root + return 0; + } + // TODO: Fix hardcoded height + return RoomBubbleCellLayout.threadSummaryViewTopMargin + 30; +} + - (CGFloat)urlPreviewHeightForEventId:(NSString*)eventId { MXKRoomBubbleComponent *component = [self bubbleComponentWithLinkForEventId:eventId]; diff --git a/Riot/Modules/Room/Views/BubbleCells/Encryption/RoomBubbleCellLayout.swift b/Riot/Modules/Room/Views/BubbleCells/Encryption/RoomBubbleCellLayout.swift index b9d1db6e7..a860d942b 100644 --- a/Riot/Modules/Room/Views/BubbleCells/Encryption/RoomBubbleCellLayout.swift +++ b/Riot/Modules/Room/Views/BubbleCells/Encryption/RoomBubbleCellLayout.swift @@ -46,4 +46,8 @@ final class RoomBubbleCellLayout: NSObject { static let encryptedContentLeftMargin: CGFloat = 15.0 static let urlPreviewViewTopMargin: CGFloat = 8.0 + + // Threads + + static let threadSummaryViewTopMargin: CGFloat = 8.0 } From ca84e052d2de7b2c449effcd8672f62428099d92 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Tue, 9 Nov 2021 15:18:18 +0300 Subject: [PATCH 06/87] Add labs setting for threads --- Riot/Assets/en.lproj/Vector.strings | 1 + Riot/Generated/Strings.swift | 4 ++++ .../Modules/Settings/SettingsViewController.m | 22 ++++++++++++++++++- 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 7c0ad13ab..5f1296a4d 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -569,6 +569,7 @@ Tap the + to start adding people."; "settings_labs_message_reaction" = "React to messages with emoji"; "settings_labs_enable_ringing_for_group_calls" = "Ring for group calls"; "settings_labs_voice_messages" = "Voice messages"; +"settings_labs_enable_threads" = "Threaded messaging"; "settings_version" = "Version %@"; "settings_olm_version" = "Olm Version %@"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index b3f92c58c..a72ab4dae 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -4511,6 +4511,10 @@ public class VectorL10n: NSObject { public static var settingsLabsEnableRingingForGroupCalls: String { return VectorL10n.tr("Vector", "settings_labs_enable_ringing_for_group_calls") } + /// Threaded messaging + public static var settingsLabsEnableThreads: String { + return VectorL10n.tr("Vector", "settings_labs_enable_threads") + } /// React to messages with emoji public static var settingsLabsMessageReaction: String { return VectorL10n.tr("Vector", "settings_labs_message_reaction") diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 7ecfcb533..69f18510d 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -155,7 +155,8 @@ enum enum { - LABS_ENABLE_RINGING_FOR_GROUP_CALLS_INDEX = 0 + LABS_ENABLE_RINGING_FOR_GROUP_CALLS_INDEX = 0, + LABS_ENABLE_THREADS_INDEX }; enum @@ -556,6 +557,7 @@ TableViewSectionsDelegate> { Section *sectionLabs = [Section sectionWithTag:SECTION_TAG_LABS]; [sectionLabs addRowWithTag:LABS_ENABLE_RINGING_FOR_GROUP_CALLS_INDEX]; + [sectionLabs addRowWithTag:LABS_ENABLE_THREADS_INDEX]; sectionLabs.headerTitle = [VectorL10n settingsLabs]; if (sectionLabs.hasAnyRows) { @@ -2360,6 +2362,18 @@ TableViewSectionsDelegate> [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleEnableRingingForGroupCalls:) forControlEvents:UIControlEventValueChanged]; + cell = labelAndSwitchCell; + } + else if (row == LABS_ENABLE_THREADS_INDEX) + { + MXKTableViewCellWithLabelAndSwitch *labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; + + labelAndSwitchCell.mxkLabel.text = [VectorL10n settingsLabsEnableThreads]; + labelAndSwitchCell.mxkSwitch.on = RiotSettings.shared.enableThreads; + labelAndSwitchCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor; + + [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleEnableThreads:) forControlEvents:UIControlEventValueChanged]; + cell = labelAndSwitchCell; } } @@ -3093,6 +3107,12 @@ TableViewSectionsDelegate> RiotSettings.shared.enableRingingForGroupCalls = sender.isOn; } +- (void)toggleEnableThreads:(UISwitch *)sender +{ + RiotSettings.shared.enableThreads = sender.isOn; + [[MXKRoomDataSourceManager sharedManagerForMatrixSession:self.mainSession] reset]; +} + - (void)togglePinRoomsWithMissedNotif:(UISwitch *)sender { RiotSettings.shared.pinRoomsWithMissedNotificationsOnHome = sender.isOn; From 7267a0889d9ee8928ac9ecfaed7d73b19b2d44bc Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Tue, 9 Nov 2021 15:24:08 +0300 Subject: [PATCH 07/87] Add thread option to an event if the labs setting enabled --- Riot/Assets/en.lproj/Vector.strings | 2 +- Riot/Generated/Strings.swift | 2 +- Riot/Modules/Room/RoomViewController.m | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 5f1296a4d..470d081dc 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -378,7 +378,7 @@ Tap the + to start adding people."; "room_event_action_cancel_download" = "Cancel Download"; "room_event_action_view_encryption" = "Encryption Information"; "room_event_action_reply" = "Reply"; -"room_event_action_reply_in_thread" = "Reply in thread"; +"room_event_action_reply_in_thread" = "Thread"; "room_event_action_edit" = "Edit"; "room_event_action_reaction_show_all" = "Show all"; "room_event_action_reaction_show_less" = "Show less"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index a72ab4dae..44c4a77b3 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -2927,7 +2927,7 @@ public class VectorL10n: NSObject { public static var roomEventActionReply: String { return VectorL10n.tr("Vector", "room_event_action_reply") } - /// Reply in thread + /// Thread public static var roomEventActionReplyInThread: String { return VectorL10n.tr("Vector", "room_event_action_reply_in_thread") } diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 07c1b6859..9c7f71389 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -5931,7 +5931,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [items addObject:[self copyMenuItemWithEvent:event andCell:cell]]; [items addObject:[self replyMenuItemWithEvent:event]]; - if (!event.threadIdentifier) + if (RiotSettings.shared.enableThreads && !event.threadIdentifier) { // add "reply in thread" option only if not a thread already [items addObject:[self replyInThreadMenuItemWithEvent:event]]; From b5ead490e19cc3f09238ee76fb1ae3c363be5c98 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Tue, 9 Nov 2021 16:50:02 +0300 Subject: [PATCH 08/87] Create ThreadSummaryView --- .../Room/CellData/RoomBubbleCellData.m | 3 +- .../Views/Threads/ThreadSummaryView.swift | 110 ++++++++++++++++++ .../Room/Views/Threads/ThreadSummaryView.xib | 92 +++++++++++++++ 3 files changed, 203 insertions(+), 2 deletions(-) create mode 100644 Riot/Modules/Room/Views/Threads/ThreadSummaryView.swift create mode 100644 Riot/Modules/Room/Views/Threads/ThreadSummaryView.xib diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.m b/Riot/Modules/Room/CellData/RoomBubbleCellData.m index 72a7ae53b..89ef2578b 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.m +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.m @@ -625,8 +625,7 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat // component is not a thread root return 0; } - // TODO: Fix hardcoded height - return RoomBubbleCellLayout.threadSummaryViewTopMargin + 30; + return RoomBubbleCellLayout.threadSummaryViewTopMargin + [ThreadSummaryView contentViewHeightForThread:component.thread fitting:self.maxTextViewWidth]; } - (CGFloat)urlPreviewHeightForEventId:(NSString*)eventId diff --git a/Riot/Modules/Room/Views/Threads/ThreadSummaryView.swift b/Riot/Modules/Room/Views/Threads/ThreadSummaryView.swift new file mode 100644 index 000000000..9d79273bd --- /dev/null +++ b/Riot/Modules/Room/Views/Threads/ThreadSummaryView.swift @@ -0,0 +1,110 @@ +// +// 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 Foundation +import Reusable + +@objc +protocol ThreadSummaryViewDelegate: AnyObject { + func tappedThreadSummaryView(_ summaryView: ThreadSummaryView, for thread: MXThread) +} + +/// A view to display a summary for an `MXThread` generated by the `MXThreadingService`. +@objcMembers +class ThreadSummaryView: UIView { + + private enum Constants { + static let viewHeight: CGFloat = 32 + static let cornerRadius: CGFloat = 4 + } + + @IBOutlet private weak var iconView: UIImageView! + @IBOutlet private weak var numberOfRepliesLabel: UILabel! + @IBOutlet private weak var lastMessageAvatarView: UserAvatarView! + @IBOutlet private weak var lastMessageContentLabel: UILabel! + + private(set) var thread: MXThread! { + didSet { + configure() + } + } + + weak var delegate: ThreadSummaryViewDelegate? + + // MARK: - Setup + + static func instantiate(withThread thread: MXThread) -> ThreadSummaryView { + let view = ThreadSummaryView.loadFromNib() + view.thread = thread + view.update(theme: ThemeService.shared().theme) + view.translatesAutoresizingMaskIntoConstraints = false + return view + } + + static func contentViewHeight(forThread thread: MXThread, fitting maxWidth: CGFloat) -> CGFloat { + return Constants.viewHeight + } + + private func configure() { + clipsToBounds = true + layer.cornerRadius = Constants.cornerRadius + + guard let thread = thread else { return } + numberOfRepliesLabel.text = String(thread.numberOfReplies) + guard let lastMessage = thread.lastMessage else { + lastMessageAvatarView.avatarImageView.image = nil + lastMessageContentLabel.text = nil + return + } + guard let session = thread.session else { return } + let lastMessageSender = session.user(withUserId: lastMessage.sender) + + let fallbackImage = AvatarFallbackImage.matrixItem(lastMessage.sender, + lastMessageSender?.displayname) + let avatarViewData = AvatarViewData(matrixItemId: lastMessage.sender, + displayName: lastMessageSender?.displayname, + avatarUrl: lastMessageSender?.avatarUrl, + mediaManager: session.mediaManager, + fallbackImage: fallbackImage) + lastMessageAvatarView.fill(with: avatarViewData) + + guard let eventFormatter = session.roomSummaryUpdateDelegate as? MXKEventFormatter, + let room = session.room(withRoomId: lastMessage.roomId) else { + return + } + + room.state { [weak self] roomState in + guard let self = self else { return } + let formatterError = UnsafeMutablePointer.allocate(capacity: 1) + self.lastMessageContentLabel.text = eventFormatter.string(from: lastMessage, with: roomState, error: formatterError) + } + } + + // MARK: - Action + +} + +extension ThreadSummaryView: NibLoadable {} + +extension ThreadSummaryView: Themable { + + func update(theme: Theme) { + backgroundColor = theme.colors.system + numberOfRepliesLabel.textColor = theme.colors.secondaryContent + lastMessageContentLabel.textColor = theme.colors.secondaryContent + } + +} diff --git a/Riot/Modules/Room/Views/Threads/ThreadSummaryView.xib b/Riot/Modules/Room/Views/Threads/ThreadSummaryView.xib new file mode 100644 index 000000000..15df38c54 --- /dev/null +++ b/Riot/Modules/Room/Views/Threads/ThreadSummaryView.xib @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 55669ed8d30f2509424f155a2dcf69f3fec0eae6 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Tue, 9 Nov 2021 16:50:22 +0300 Subject: [PATCH 09/87] Add ThreadSummaryView into the cell --- .../Modules/Room/DataSources/RoomDataSource.m | 77 +++++++++++++------ 1 file changed, 55 insertions(+), 22 deletions(-) diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m index a83ba10da..9df1dbe35 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.m +++ b/Riot/Modules/Room/DataSources/RoomDataSource.m @@ -343,7 +343,7 @@ const CGFloat kTypingCellHeight = 24; // Handle read receipts and read marker display. // Ignore the read receipts on the bubble without actual display. // Ignore the read receipts on collapsed bubbles - if ((((self.showBubbleReceipts && cellData.readReceipts.count) || cellData.reactions.count || cellData.hasLink) && !isCollapsableCellCollapsed) || self.showReadMarker) + if ((((self.showBubbleReceipts && cellData.readReceipts.count) || cellData.reactions.count || cellData.hasLink || cellData.hasThreadRoot) && !isCollapsableCellCollapsed) || self.showReadMarker) { // Read receipts container are inserted here on the right side into the content view. // Some vertical whitespaces are added in message text view (see RoomBubbleCellData class) to insert correctly multiple receipts. @@ -380,11 +380,6 @@ const CGFloat kTypingCellHeight = 24; urlPreviewView.tag = index; [temporaryViews addObject:urlPreviewView]; - - if (!bubbleCell.tmpSubviews) - { - bubbleCell.tmpSubviews = [NSMutableArray array]; - } [bubbleCell.tmpSubviews addObject:urlPreviewView]; urlPreviewView.translatesAutoresizingMaskIntoConstraints = NO; @@ -423,11 +418,6 @@ const CGFloat kTypingCellHeight = 24; bubbleReactionsViewModel.viewModelDelegate = self; [temporaryViews addObject:reactionsView]; - - if (!bubbleCell.tmpSubviews) - { - bubbleCell.tmpSubviews = [NSMutableArray array]; - } [bubbleCell.tmpSubviews addObject:reactionsView]; if ([[bubbleCell class] conformsToProtocol:@protocol(BubbleCellReactionsDisplayable)]) @@ -468,6 +458,53 @@ const CGFloat kTypingCellHeight = 24; } } + ThreadSummaryView *threadSummaryView; + + // display thread summary view if the component has a thread in the room timeline + if (RiotSettings.shared.enableThreads && component.thread && !self.threadId) + { + threadSummaryView = [ThreadSummaryView instantiateWithThread:component.thread]; + + [temporaryViews addObject:threadSummaryView]; + [bubbleCell.tmpSubviews addObject:threadSummaryView]; + + threadSummaryView.translatesAutoresizingMaskIntoConstraints = NO; + [bubbleCell.contentView addSubview:threadSummaryView]; + + CGFloat leftMargin = RoomBubbleCellLayout.reactionsViewLeftMargin; + if (roomBubbleCellData.containsBubbleComponentWithEncryptionBadge) + { + leftMargin+= RoomBubbleCellLayout.encryptedContentLeftMargin; + } + + // The top constraint may need to include the URL preview view or reactions view + NSLayoutConstraint *topConstraint; + if (reactionsView) + { + topConstraint = [threadSummaryView.topAnchor constraintEqualToAnchor:reactionsView.bottomAnchor + constant:RoomBubbleCellLayout.threadSummaryViewTopMargin]; + } + else if (urlPreviewView) + { + topConstraint = [threadSummaryView.topAnchor constraintEqualToAnchor:urlPreviewView.bottomAnchor + constant:RoomBubbleCellLayout.threadSummaryViewTopMargin]; + } + else + { + topConstraint = [threadSummaryView.topAnchor constraintEqualToAnchor:threadSummaryView.superview.topAnchor + constant:bottomPositionY + RoomBubbleCellLayout.threadSummaryViewTopMargin]; + } + + // Set constraints for the summary view + [NSLayoutConstraint activateConstraints: @[ + [threadSummaryView.leadingAnchor constraintEqualToAnchor:threadSummaryView.superview.leadingAnchor + constant:leftMargin], + topConstraint, + [threadSummaryView.heightAnchor constraintEqualToConstant:[ThreadSummaryView contentViewHeightForThread:component.thread fitting:cellData.maxTextViewWidth]], + [threadSummaryView.trailingAnchor constraintEqualToAnchor:threadSummaryView.superview.trailingAnchor constant:-RoomBubbleCellLayout.reactionsViewRightMargin] + ]]; + } + MXKReceiptSendersContainer* avatarsContainer; // Handle read receipts (if any) @@ -523,16 +560,8 @@ const CGFloat kTypingCellHeight = 24; avatarsContainer.accessibilityIdentifier = @"readReceiptsContainer"; [temporaryViews addObject:avatarsContainer]; - // Add this read receipts container in the content view - if (!bubbleCell.tmpSubviews) - { - bubbleCell.tmpSubviews = [NSMutableArray arrayWithArray:@[avatarsContainer]]; - } - else - { - [bubbleCell.tmpSubviews addObject:avatarsContainer]; - } + [bubbleCell.tmpSubviews addObject:avatarsContainer]; if ([[bubbleCell class] conformsToProtocol:@protocol(BubbleCellReadReceiptsDisplayable)]) { @@ -569,9 +598,13 @@ const CGFloat kTypingCellHeight = 24; multiplier:1.0 constant:-RoomBubbleCellLayout.readReceiptsViewRightMargin]; - // At the bottom, we either have reactions, a URL preview or nothing + // At the bottom, we either have a thread summary, a reactions, a URL preview or nothing NSLayoutConstraint *topConstraint; - if (reactionsView) + if (threadSummaryView) + { + topConstraint = [avatarsContainer.topAnchor constraintEqualToAnchor:threadSummaryView.bottomAnchor constant:RoomBubbleCellLayout.readReceiptsViewTopMargin]; + } + else if (reactionsView) { topConstraint = [avatarsContainer.topAnchor constraintEqualToAnchor:reactionsView.bottomAnchor constant:RoomBubbleCellLayout.readReceiptsViewTopMargin]; } From 93b037240f48abbe750918605854154e34d532eb Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Wed, 10 Nov 2021 13:38:32 +0300 Subject: [PATCH 10/87] Implement thread filtering logic in RoomDataSource --- .../Modules/Room/DataSources/RoomDataSource.m | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m index 9df1dbe35..5d04ac36e 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.m +++ b/Riot/Modules/Room/DataSources/RoomDataSource.m @@ -239,6 +239,52 @@ const CGFloat kTypingCellHeight = 24; [self enableRoomCreationIntroCellDisplayIfNeeded]; } +- (BOOL)shouldQueueEventForProcessing:(MXEvent *)event roomState:(MXRoomState *)roomState direction:(MXTimelineDirection)direction +{ + if (self.threadId) + { + // if in a thread, ignore non-root event or events from other threads + if (![event.eventId isEqualToString:self.threadId] && ![event.threadIdentifier isEqualToString:self.threadId]) + { + // Ignore the event + return NO; + } + // also ignore events related to un-threaded or events from other threads + if (!event.isInThread && event.relatesTo.eventId) + { + MXEvent *relatedEvent = [self.mxSession.store eventWithEventId:event.relatesTo.eventId + inRoom:event.roomId]; + if (![relatedEvent.threadIdentifier isEqualToString:self.threadId]) + { + // ignore the event + return NO; + } + } + } + else + { + // if not in a thread, ignore all threaded events + if (event.threadIdentifier) + { + // ignore the event + return NO; + } + // also ignore events related to threaded events + if (event.relatesTo.eventId) + { + MXEvent *relatedEvent = [self.mxSession.store eventWithEventId:event.relatesTo.eventId + inRoom:event.roomId]; + if (relatedEvent.threadIdentifier) + { + // ignore the event + return NO; + } + } + } + + return [super shouldQueueEventForProcessing:event roomState:roomState direction:direction]; +} + #pragma mark - - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section From cc1872358829e053359373550cac1a403d31a056 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Wed, 10 Nov 2021 15:42:42 +0300 Subject: [PATCH 11/87] Implement opening thread modal when tapped --- .../Modules/Room/DataSources/RoomDataSource.h | 38 ++++++++++++------- .../Modules/Room/DataSources/RoomDataSource.m | 19 +++++++--- Riot/Modules/Room/RoomViewController.m | 7 +++- .../Views/Threads/ThreadSummaryView.swift | 12 +++++- 4 files changed, 55 insertions(+), 21 deletions(-) diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.h b/Riot/Modules/Room/DataSources/RoomDataSource.h index 725171572..1283e0ffb 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.h +++ b/Riot/Modules/Room/DataSources/RoomDataSource.h @@ -33,7 +33,7 @@ /** The event id of the current selected event if any. Default is nil. */ -@property(nonatomic) NSString *selectedEventId; +@property(nonatomic, nullable) NSString *selectedEventId; /** Tell whether the initial event of the timeline (if any) must be marked. Default is NO. @@ -60,7 +60,7 @@ @return a widget representating the active jitsi conference in the room. Else, nil. */ -- (Widget *)jitsiWidget; +- (Widget * _Nullable)jitsiWidget; /** Send a video to the room. @@ -74,9 +74,9 @@ the event id of the event generated on the homeserver @param failure A block object called when the operation fails. */ -- (void)sendVideo:(NSURL*)videoLocalURL - success:(void (^)(NSString *eventId))success - failure:(void (^)(NSError *error))failure; +- (void)sendVideo:(NSURL * _Nonnull)videoLocalURL + success:(nullable void (^)(NSString * _Nonnull))success + failure:(nullable void (^)(NSError * _Nullable))failure; /** Accept incoming key verification request. @@ -85,9 +85,9 @@ @param success A block object called when the operation succeeds. @param failure A block object called when the operation fails. */ -- (void)acceptVerificationRequestForEventId:(NSString*)eventId - success:(void(^)(void))success - failure:(void(^)(NSError*))failure; +- (void)acceptVerificationRequestForEventId:(NSString * _Nonnull)eventId + success:(nullable void(^)(void))success + failure:(nullable void(^)(NSError * _Nullable))failure; /** Decline incoming key verification request. @@ -96,9 +96,9 @@ @param success A block object called when the operation succeeds. @param failure A block object called when the operation fails. */ -- (void)declineVerificationRequestForEventId:(NSString*)eventId - success:(void(^)(void))success - failure:(void(^)(NSError*))failure; +- (void)declineVerificationRequestForEventId:(NSString * _Nonnull)eventId + success:(nullable void(^)(void))success + failure:(nullable void(^)(NSError * _Nullable))failure; - (void)resetTypingNotification; @@ -106,9 +106,19 @@ @protocol RoomDataSourceDelegate -- (void)roomDataSource:(RoomDataSource*)roomDataSource didUpdateEncryptionTrustLevel:(RoomEncryptionTrustLevel)roomEncryptionTrustLevel; - -- (void)roomDataSource:(RoomDataSource*)roomDataSource didCancel:(MXEvent *)event; +/** + Called when the room's encryption trust level did updated. + + @param roomDataSource room data source instance + */ +- (void)roomDataSourceDidUpdateEncryptionTrustLevel:(RoomDataSource * _Nonnull)roomDataSource; +/** + Called when a thread summary view + + @param roomDataSource room data source instance + */ +- (void)roomDataSource:(RoomDataSource * _Nonnull)roomDataSource + didTapThread:(MXThread * _Nonnull)thread; @end diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m index 5d04ac36e..49a52ff18 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.m +++ b/Riot/Modules/Room/DataSources/RoomDataSource.m @@ -29,7 +29,7 @@ const CGFloat kTypingCellHeight = 24; -@interface RoomDataSource() +@interface RoomDataSource() { // Observe kThemeServiceDidChangeThemeNotification to handle user interface theme change. id kThemeServiceDidChangeThemeNotificationObserver; @@ -231,7 +231,7 @@ const CGFloat kTypingCellHeight = 24; - (void)fetchEncryptionTrustedLevel { self.encryptionTrustLevel = self.room.summary.roomEncryptionTrustLevel; - [self.roomDataSourceDelegate roomDataSource:self didUpdateEncryptionTrustLevel:self.encryptionTrustLevel]; + [self.roomDataSourceDelegate roomDataSourceDidUpdateEncryptionTrustLevel:self]; } - (void)roomDidSet @@ -510,6 +510,7 @@ const CGFloat kTypingCellHeight = 24; if (RiotSettings.shared.enableThreads && component.thread && !self.threadId) { threadSummaryView = [ThreadSummaryView instantiateWithThread:component.thread]; + threadSummaryView.delegate = self; [temporaryViews addObject:threadSummaryView]; [bubbleCell.tmpSubviews addObject:threadSummaryView]; @@ -1013,9 +1014,9 @@ const CGFloat kTypingCellHeight = 24; return jitsiWidget; } -- (void)sendVideo:(NSURL*)videoLocalURL - success:(void (^)(NSString *eventId))success - failure:(void (^)(NSError *error))failure +- (void)sendVideo:(NSURL *)videoLocalURL + success:(void (^)(NSString * _Nonnull))success + failure:(void (^)(NSError * _Nullable))failure { AVURLAsset *videoAsset = [AVURLAsset assetWithURL:videoLocalURL]; UIImage *videoThumbnail = [MXKVideoThumbnailGenerator.shared generateThumbnailFrom:videoLocalURL]; @@ -1333,4 +1334,12 @@ const CGFloat kTypingCellHeight = 24; [self refreshCells]; } +#pragma mark - ThreadSummaryViewDelegate + +- (void)threadSummaryViewTapped:(ThreadSummaryView *)summaryView +{ + [self.roomDataSourceDelegate roomDataSource:self + didTapThread:summaryView.thread]; +} + @end diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 9c7f71389..881188d63 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -4032,12 +4032,17 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; #pragma mark - RoomDataSourceDelegate -- (void)roomDataSource:(RoomDataSource *)roomDataSource didUpdateEncryptionTrustLevel:(RoomEncryptionTrustLevel)roomEncryptionTrustLevel +- (void)roomDataSourceDidUpdateEncryptionTrustLevel:(RoomDataSource *)roomDataSource { [self updateInputToolbarEncryptionDecoration]; [self updateTitleViewEncryptionDecoration]; } +- (void)roomDataSource:(RoomDataSource *)roomDataSource didTapThread:(MXThread *)thread +{ + [self openThreadWithId:thread.identifier]; +} + #pragma mark - Segues - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender diff --git a/Riot/Modules/Room/Views/Threads/ThreadSummaryView.swift b/Riot/Modules/Room/Views/Threads/ThreadSummaryView.swift index 9d79273bd..ff5d65c48 100644 --- a/Riot/Modules/Room/Views/Threads/ThreadSummaryView.swift +++ b/Riot/Modules/Room/Views/Threads/ThreadSummaryView.swift @@ -19,7 +19,7 @@ import Reusable @objc protocol ThreadSummaryViewDelegate: AnyObject { - func tappedThreadSummaryView(_ summaryView: ThreadSummaryView, for thread: MXThread) + func threadSummaryViewTapped(_ summaryView: ThreadSummaryView) } /// A view to display a summary for an `MXThread` generated by the `MXThreadingService`. @@ -42,6 +42,10 @@ class ThreadSummaryView: UIView { } } + private lazy var tapGestureRecognizer: UITapGestureRecognizer = { + return UITapGestureRecognizer(target: self, action: #selector(tapped(_:))) + }() + weak var delegate: ThreadSummaryViewDelegate? // MARK: - Setup @@ -61,6 +65,7 @@ class ThreadSummaryView: UIView { private func configure() { clipsToBounds = true layer.cornerRadius = Constants.cornerRadius + addGestureRecognizer(tapGestureRecognizer) guard let thread = thread else { return } numberOfRepliesLabel.text = String(thread.numberOfReplies) @@ -95,6 +100,11 @@ class ThreadSummaryView: UIView { // MARK: - Action + @objc + private func tapped(_ sender: UITapGestureRecognizer) { + guard thread != nil else { return } + delegate?.threadSummaryViewTapped(self) + } } extension ThreadSummaryView: NibLoadable {} From f26601196ac5cf400e9d7ef88d63f4832cbf3b77 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Wed, 10 Nov 2021 18:03:02 +0300 Subject: [PATCH 12/87] Introduce RoomDisplayConfiguration for room display options --- Riot/Modules/Room/RoomCoordinator.swift | 10 ++++- .../Room/RoomCoordinatorBridgePresenter.swift | 8 +++- .../Room/RoomCoordinatorParameters.swift | 26 +++++++++-- .../Room/RoomDisplayConfiguration.swift | 44 +++++++++++++++++++ Riot/Modules/Room/RoomViewController.h | 10 ++++- Riot/Modules/Room/RoomViewController.m | 40 +++++++++++++++-- .../Modules/Thread/ThreadViewController.swift | 30 +++++++++++++ 7 files changed, 158 insertions(+), 10 deletions(-) create mode 100644 Riot/Modules/Room/RoomDisplayConfiguration.swift create mode 100644 Riot/Modules/Thread/ThreadViewController.swift diff --git a/Riot/Modules/Room/RoomCoordinator.swift b/Riot/Modules/Room/RoomCoordinator.swift index 03ad0b977..c8b260e5e 100644 --- a/Riot/Modules/Room/RoomCoordinator.swift +++ b/Riot/Modules/Room/RoomCoordinator.swift @@ -72,7 +72,12 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { self.parameters = parameters self.selectedEventId = parameters.eventId - self.roomViewController = RoomViewController.instantiate() + if let threadId = parameters.threadId { + self.roomViewController = ThreadViewController.instantiate(withThreadId: threadId, + configuration: parameters.displayConfiguration) + } else { + self.roomViewController = RoomViewController.instantiate(with: parameters.displayConfiguration) + } self.activityIndicatorPresenter = ActivityIndicatorPresenter() super.init() @@ -127,6 +132,9 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { } func toPresentable() -> UIViewController { + if parameters.threadId != nil { + return self.navigationRouter?.toPresentable() ?? self.roomViewController + } return self.roomViewController } diff --git a/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift b/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift index 7960dee18..9616969fa 100644 --- a/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift +++ b/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift @@ -37,6 +37,9 @@ class RoomCoordinatorBridgePresenterParameters: NSObject { /// If not nil, specified thread will be opened. let threadId: String? + /// Display configuration for the room + let displayConfiguration: RoomDisplayConfiguration + /// The data for the room preview. let previewData: RoomPreviewData? @@ -44,11 +47,13 @@ class RoomCoordinatorBridgePresenterParameters: NSObject { roomId: String, eventId: String?, threadId: String?, + displayConfiguration: RoomDisplayConfiguration, previewData: RoomPreviewData?) { self.session = session self.roomId = roomId self.eventId = eventId self.threadId = threadId + self.displayConfiguration = displayConfiguration self.previewData = previewData } } @@ -126,7 +131,8 @@ final class RoomCoordinatorBridgePresenter: NSObject { session: self.bridgeParameters.session, roomId: self.bridgeParameters.roomId, eventId: self.bridgeParameters.eventId, - threadId: self.bridgeParameters.threadId) + threadId: self.bridgeParameters.threadId, + displayConfiguration: self.bridgeParameters.displayConfiguration) } return RoomCoordinator(parameters: coordinatorParameters) diff --git a/Riot/Modules/Room/RoomCoordinatorParameters.swift b/Riot/Modules/Room/RoomCoordinatorParameters.swift index 1ed73f8cf..13c47a039 100644 --- a/Riot/Modules/Room/RoomCoordinatorParameters.swift +++ b/Riot/Modules/Room/RoomCoordinatorParameters.swift @@ -40,6 +40,9 @@ struct RoomCoordinatorParameters { /// If not nil, specified thread will be opened. let threadId: String? + /// Display configuration for the room + let displayConfiguration: RoomDisplayConfiguration + /// The data for the room preview. let previewData: RoomPreviewData? @@ -51,6 +54,7 @@ struct RoomCoordinatorParameters { roomId: String, eventId: String?, threadId: String?, + displayConfiguration: RoomDisplayConfiguration, previewData: RoomPreviewData?) { self.navigationRouter = navigationRouter self.navigationRouterStore = navigationRouterStore @@ -58,6 +62,7 @@ struct RoomCoordinatorParameters { self.roomId = roomId self.eventId = eventId self.threadId = threadId + self.displayConfiguration = displayConfiguration self.previewData = previewData } @@ -67,9 +72,17 @@ struct RoomCoordinatorParameters { session: MXSession, roomId: String, eventId: String? = nil, - threadId: String? = nil) { + threadId: String? = nil, + displayConfiguration: RoomDisplayConfiguration = .default) { - self.init(navigationRouter: navigationRouter, navigationRouterStore: navigationRouterStore, session: session, roomId: roomId, eventId: eventId, threadId: threadId, previewData: nil) + self.init(navigationRouter: navigationRouter, + navigationRouterStore: navigationRouterStore, + session: session, + roomId: roomId, + eventId: eventId, + threadId: threadId, + displayConfiguration: displayConfiguration, + previewData: nil) } /// Init to present a room preview @@ -77,6 +90,13 @@ struct RoomCoordinatorParameters { navigationRouterStore: NavigationRouterStoreProtocol? = nil, previewData: RoomPreviewData) { - self.init(navigationRouter: navigationRouter, navigationRouterStore: navigationRouterStore, session: previewData.mxSession, roomId: previewData.roomId, eventId: nil, threadId: nil, previewData: previewData) + self.init(navigationRouter: navigationRouter, + navigationRouterStore: navigationRouterStore, + session: previewData.mxSession, + roomId: previewData.roomId, + eventId: nil, + threadId: nil, + displayConfiguration: .default, + previewData: previewData) } } diff --git a/Riot/Modules/Room/RoomDisplayConfiguration.swift b/Riot/Modules/Room/RoomDisplayConfiguration.swift new file mode 100644 index 000000000..2c3ab1ab8 --- /dev/null +++ b/Riot/Modules/Room/RoomDisplayConfiguration.swift @@ -0,0 +1,44 @@ +// +// 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 Foundation + +@objcMembers +class RoomDisplayConfiguration: NSObject { + + let callsEnabled: Bool + + let integrationsEnabled: Bool + + let jitsiWidgetRemoverEnabled: Bool + + init(callsEnabled: Bool, + integrationsEnabled: Bool, + jitsiWidgetRemoverEnabled: Bool) { + self.callsEnabled = callsEnabled + self.integrationsEnabled = integrationsEnabled + self.jitsiWidgetRemoverEnabled = jitsiWidgetRemoverEnabled + super.init() + } + + static let `default`: RoomDisplayConfiguration = RoomDisplayConfiguration(callsEnabled: true, + integrationsEnabled: true, + jitsiWidgetRemoverEnabled: true) + + static let forThreads: RoomDisplayConfiguration = RoomDisplayConfiguration(callsEnabled: false, + integrationsEnabled: false, + jitsiWidgetRemoverEnabled: false) +} diff --git a/Riot/Modules/Room/RoomViewController.h b/Riot/Modules/Room/RoomViewController.h index 5016dec0f..1649e36b0 100644 --- a/Riot/Modules/Room/RoomViewController.h +++ b/Riot/Modules/Room/RoomViewController.h @@ -30,6 +30,7 @@ @class BadgeLabel; @class UniversalLinkParameters; @protocol RoomViewControllerDelegate; +@class RoomDisplayConfiguration; NS_ASSUME_NONNULL_BEGIN @@ -72,6 +73,11 @@ extern NSNotificationName const RoomGroupCallTileTappedNotification; */ @property (nonatomic, readonly, nullable) RoomPreviewData *roomPreviewData; +/** + Display configuration for the room view controller. + */ +@property (nonatomic, readonly) RoomDisplayConfiguration *displayConfiguration; + /** Tell whether a badge must be added next to the chevron (back button) showing number of unread rooms. YES by default. @@ -97,9 +103,11 @@ extern NSNotificationName const RoomGroupCallTileTappedNotification; /** Creates and returns a new `RoomViewController` object. + @param configuration display configuration for the room view controller. + @return An initialized `RoomViewController` object. */ -+ (instancetype)instantiate; ++ (instancetype)instantiateWithConfiguration:(RoomDisplayConfiguration *)configuration; @end diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 881188d63..06c42ab59 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -258,6 +258,8 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; @property (nonatomic, strong) UserSuggestionCoordinatorBridge *userSuggestionCoordinator; @property (nonatomic, weak) IBOutlet UIView *userSuggestionContainerView; +@property (nonatomic, readwrite) RoomDisplayConfiguration *displayConfiguration; + @end @implementation RoomViewController @@ -273,14 +275,18 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; + (instancetype)roomViewController { - return [[[self class] alloc] initWithNibName:NSStringFromClass(self.class) - bundle:[NSBundle bundleForClass:self.class]]; + RoomViewController *controller = [[[self class] alloc] initWithNibName:NSStringFromClass(self.class) + bundle:[NSBundle bundleForClass:self.class]]; + controller.displayConfiguration = [RoomDisplayConfiguration default]; + return controller; } -+ (instancetype)instantiate ++ (instancetype)instantiateWithConfiguration:(RoomDisplayConfiguration *)configuration { UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:[NSBundle mainBundle]]; - return [storyboard instantiateViewControllerWithIdentifier:@"RoomViewControllerStoryboardId"]; + RoomViewController *controller = [storyboard instantiateViewControllerWithIdentifier:@"RoomViewControllerStoryboardId"]; + controller.displayConfiguration = configuration; + return controller; } #pragma mark - @@ -1502,6 +1508,11 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; - (void)setupRemoveJitsiWidgetRemoveView { + if (!self.displayConfiguration.jitsiWidgetRemoverEnabled) + { + return; + } + self.removeJitsiWidgetView = [RemoveJitsiWidgetView instantiate]; self.removeJitsiWidgetView.delegate = self; @@ -1544,6 +1555,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; - (BOOL)supportCallOption { + if (!self.displayConfiguration.callsEnabled) + { + return NO; + } BOOL callOptionAllowed = (self.roomDataSource.room.isDirect && RiotSettings.shared.roomScreenAllowVoIPForDirectRoom) || (!self.roomDataSource.room.isDirect && RiotSettings.shared.roomScreenAllowVoIPForNonDirectRoom); return callOptionAllowed && BuildSettings.allowVoIPUsage && self.roomDataSource.mxSession.callManager && self.roomDataSource.room.summary.membersCount.joined >= 2; } @@ -4883,6 +4898,11 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; - (void)listenWidgetNotifications { + if (!self.displayConfiguration.jitsiWidgetRemoverEnabled) + { + return; + } + MXWeakify(self); kMXKWidgetManagerDidUpdateWidgetObserver = [[NSNotificationCenter defaultCenter] addObserverForName:kWidgetManagerDidUpdateWidgetNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { @@ -4919,6 +4939,11 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; - (NSUInteger)widgetsCount:(BOOL)includeUserWidgets { + if (!self.displayConfiguration.integrationsEnabled) + { + return 0; + } + NSUInteger widgetsCount = [[WidgetManager sharedManager] widgetsNotOfTypes:@[kWidgetTypeJitsiV1, kWidgetTypeJitsiV2] inRoom:self.roomDataSource.room withRoomState:self.roomDataSource.roomState].count; @@ -5549,6 +5574,11 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; - (void)refreshRemoveJitsiWidgetView { + if (!self.displayConfiguration.jitsiWidgetRemoverEnabled) + { + return; + } + if (self.roomDataSource.isLive && !self.roomDataSource.isPeeking) { Widget *jitsiWidget = [customizedRoomDataSource jitsiWidget]; @@ -6287,10 +6317,12 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; self.threadBridgePresenter = nil; } + RoomDisplayConfiguration *configuration = RoomDisplayConfiguration.forThreads; RoomCoordinatorBridgePresenterParameters *parameters = [[RoomCoordinatorBridgePresenterParameters alloc] initWithSession:self.mainSession roomId:self.roomDataSource.roomId eventId:nil threadId:threadId + displayConfiguration:configuration previewData:nil]; RoomCoordinatorBridgePresenter *presenter = [[RoomCoordinatorBridgePresenter alloc] initWithParameters:parameters]; self.threadBridgePresenter = presenter; diff --git a/Riot/Modules/Thread/ThreadViewController.swift b/Riot/Modules/Thread/ThreadViewController.swift new file mode 100644 index 000000000..1639d0db9 --- /dev/null +++ b/Riot/Modules/Thread/ThreadViewController.swift @@ -0,0 +1,30 @@ +// +// 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 Foundation + +class ThreadViewController: RoomViewController { + + private(set) var threadId: String! + + class func instantiate(withThreadId threadId: String, + configuration: RoomDisplayConfiguration) -> ThreadViewController { + let threadVC = ThreadViewController.instantiate(with: configuration) + threadVC.threadId = threadId + return threadVC + } + +} From c2621ad19b5f9124c3c604e0b28116ba5f4796a6 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Thu, 11 Nov 2021 01:02:47 +0300 Subject: [PATCH 13/87] Create thread title string --- Riot/Assets/en.lproj/Vector.strings | 6 +++++- Riot/Generated/Strings.swift | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 470d081dc..d1b9a91de 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -323,7 +323,8 @@ Tap the + to start adding people."; "room_member_power_level_short_moderator" = "Mod"; "room_member_power_level_short_custom" = "Custom"; -// Chat +// MARK: - Chat + "room_slide_to_end_group_call" = "Slide to end the call for everyone"; "room_jump_to_first_unread" = "Jump to unread"; "room_accessiblity_scroll_to_bottom" = "Scroll to bottom"; @@ -413,6 +414,9 @@ Tap the + to start adding people."; "room_join_group_call" = "Join"; "room_no_privileges_to_create_group_call" = "You need to be an admin or a moderator to start a call."; +// MARK: Threads +"room_thread_title" = "Thread"; + "media_type_accessibility_image" = "Image"; "media_type_accessibility_audio" = "Audio"; "media_type_accessibility_video" = "Video"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 44c4a77b3..50cdb1ec6 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -3531,6 +3531,10 @@ public class VectorL10n: NSObject { public static var roomSlideToEndGroupCall: String { return VectorL10n.tr("Vector", "room_slide_to_end_group_call") } + /// Thread + public static var roomThreadTitle: String { + return VectorL10n.tr("Vector", "room_thread_title") + } /// Invite members public static var roomTitleInviteMembers: String { return VectorL10n.tr("Vector", "room_title_invite_members") From d7eb6d116371d8512185df048ffb10bcc0fde379 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Thu, 11 Nov 2021 01:05:03 +0300 Subject: [PATCH 14/87] Create specific title view for threads --- .../Title/Thread/ThreadRoomTitleView.swift | 130 ++++++++++++++++++ .../Title/Thread/ThreadRoomTitleView.xib | 125 +++++++++++++++++ 2 files changed, 255 insertions(+) create mode 100644 Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.swift create mode 100644 Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.xib diff --git a/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.swift b/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.swift new file mode 100644 index 000000000..7767378c6 --- /dev/null +++ b/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.swift @@ -0,0 +1,130 @@ +// +// 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 Foundation +import MatrixKit + +@objc +enum ThreadRoomTitleViewMode: Int { + case partial + case full +} + +@objcMembers +class ThreadRoomTitleView: RoomTitleView { + + var mode: ThreadRoomTitleViewMode = .full { + didSet { + update() + } + } + var threadId: String! + + // Container views + @IBOutlet private weak var partialContainerView: UIView! + @IBOutlet private weak var fullContainerView: UIView! + + // Individual views + @IBOutlet private weak var partialTitleLabel: UILabel! + @IBOutlet private weak var fullCloseButton: UIButton! + @IBOutlet private weak var fullTitleLabel: UILabel! + @IBOutlet private weak var fullRoomAvatarView: RoomAvatarView! + @IBOutlet private weak var fullRoomNameLabel: UILabel! + @IBOutlet private weak var fullOptionsButton: UIButton! + + var closeButton: UIButton { + return fullCloseButton + } + + override class func nib() -> UINib! { + return UINib(nibName: String(describing: self), + bundle: .main) + } + + override func refreshDisplay() { + partialTitleLabel.text = VectorL10n.roomThreadTitle + fullTitleLabel.text = VectorL10n.roomThreadTitle + + guard let room = mxRoom else { + // room not initialized yet + return + } + fullRoomNameLabel.text = room.displayName + + let avatarViewData = AvatarViewData(matrixItemId: room.matrixItemId, + displayName: room.displayName, + avatarUrl: room.mxContentUri, + mediaManager: room.mxSession.mediaManager, + fallbackImage: AvatarFallbackImage.matrixItem(room.matrixItemId, + room.displayName)) + fullRoomAvatarView.fill(with: avatarViewData) + } + + override func awakeFromNib() { + super.awakeFromNib() + + update(theme: ThemeService.shared().theme) + update() + } + + override func didMoveToSuperview() { + super.didMoveToSuperview() + + if let superview = superview?.superview { + NSLayoutConstraint.activate([ + self.leadingAnchor.constraint(equalTo: superview.leadingAnchor), + self.trailingAnchor.constraint(equalTo: superview.trailingAnchor) + ]) + } + } + + private func update() { + switch mode { + case .partial: + partialContainerView.isHidden = false + fullContainerView.isHidden = true + case .full: + partialContainerView.isHidden = true + fullContainerView.isHidden = false + } + } + + // MARK: - Actions + + @IBAction private func closeButtonTapped(_ sender: UIButton) { + let gesture = UITapGestureRecognizer(target: nil, action: nil) + closeButton.addGestureRecognizer(gesture) + tapGestureDelegate.roomTitleView(self, recognizeTapGesture: gesture) + closeButton.removeGestureRecognizer(gesture) + } + + @IBAction private func optionsButtonTapped(_ sender: UIButton) { + + } + +} + +extension ThreadRoomTitleView: Themable { + + func update(theme: Theme) { + partialTitleLabel.textColor = theme.colors.primaryContent + fullCloseButton.tintColor = theme.colors.secondaryContent + fullTitleLabel.textColor = theme.colors.primaryContent + fullRoomNameLabel.textColor = theme.colors.secondaryContent + fullOptionsButton.tintColor = theme.colors.secondaryContent + } + +} diff --git a/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.xib b/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.xib new file mode 100644 index 000000000..c47ecc287 --- /dev/null +++ b/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.xib @@ -0,0 +1,125 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 628cdf34e39f3bbf603b5b4cc3f72e6a1eea6b39 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Thu, 11 Nov 2021 01:06:00 +0300 Subject: [PATCH 15/87] Load correct instance of RoomViewController from the storyboard --- Riot/Assets/Base.lproj/Main.storyboard | 20 ++++++++++++++++++++ Riot/Modules/Room/RoomViewController.m | 13 ++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/Riot/Assets/Base.lproj/Main.storyboard b/Riot/Assets/Base.lproj/Main.storyboard index 0fc069dcc..06340fad3 100644 --- a/Riot/Assets/Base.lproj/Main.storyboard +++ b/Riot/Assets/Base.lproj/Main.storyboard @@ -543,6 +543,26 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 06c42ab59..11bebcb79 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -284,11 +284,22 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; + (instancetype)instantiateWithConfiguration:(RoomDisplayConfiguration *)configuration { UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:[NSBundle mainBundle]]; - RoomViewController *controller = [storyboard instantiateViewControllerWithIdentifier:@"RoomViewControllerStoryboardId"]; + NSString *storyboardId = [NSString stringWithFormat:@"%@StoryboardId", self.className]; + RoomViewController *controller = [storyboard instantiateViewControllerWithIdentifier:storyboardId]; controller.displayConfiguration = configuration; return controller; } ++ (NSString *)className +{ + NSString *result = NSStringFromClass(self.class); + if ([result containsString:@"."]) + { + result = [result componentsSeparatedByString:@"."].lastObject; + } + return result; +} + #pragma mark - - (instancetype)initWithNibName:(nullable NSString *)nibNameOrNil bundle:(nullable NSBundle *)nibBundleOrNil From e9f7b4a7f2ca84a5b2bc62c6ce6dccb2eaba68a6 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Thu, 11 Nov 2021 01:06:50 +0300 Subject: [PATCH 16/87] Override nib and title view for ThreadViewController --- .../Modules/Thread/ThreadViewController.swift | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/Riot/Modules/Thread/ThreadViewController.swift b/Riot/Modules/Thread/ThreadViewController.swift index 1639d0db9..bc90734a3 100644 --- a/Riot/Modules/Thread/ThreadViewController.swift +++ b/Riot/Modules/Thread/ThreadViewController.swift @@ -27,4 +27,25 @@ class ThreadViewController: RoomViewController { return threadVC } + override class func nib() -> UINib! { + // reuse 'RoomViewController.xib' file as the nib + return UINib(nibName: String(describing: RoomViewController.self), bundle: .main) + } + + override func viewDidLoad() { + super.viewDidLoad() + + self.setRoomTitleViewClass(ThreadRoomTitleView.self) + + guard let threadTitleView = self.titleView as? ThreadRoomTitleView else { + return + } + + threadTitleView.threadId = threadId + } + + override func setRoomTitleViewClass(_ roomTitleViewClass: AnyClass!) { + super.setRoomTitleViewClass(ThreadRoomTitleView.self) + } + } From 63c2954c694411ceaaa903ccb19f8c4488cff301 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Thu, 11 Nov 2021 01:07:12 +0300 Subject: [PATCH 17/87] Dismiss thread when close tapped --- Riot/Modules/Room/RoomViewController.m | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 11bebcb79..d92628e2f 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -4664,6 +4664,15 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; { [self declineRoomInvitation]; } + else if ([titleView isKindOfClass:ThreadRoomTitleView.class]) + { + ThreadRoomTitleView *threadTitleView = (ThreadRoomTitleView *)titleView; + if (tappedView == threadTitleView.closeButton) + { + // dismiss self + [self dismissViewControllerAnimated:YES completion:nil]; + } + } } - (void)declineRoomInvitation From 38517a707ad3580b3f10c2e37b1a7ea9b5cae0c1 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Thu, 11 Nov 2021 01:39:14 +0300 Subject: [PATCH 18/87] Update title view mode according to thread status --- .../Title/Thread/ThreadRoomTitleView.swift | 29 +++++++++++++++++-- .../Modules/Thread/ThreadViewController.swift | 10 ++----- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.swift b/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.swift index 7767378c6..592071dcf 100644 --- a/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.swift +++ b/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.swift @@ -31,7 +31,11 @@ class ThreadRoomTitleView: RoomTitleView { update() } } - var threadId: String! + var threadId: String! { + didSet { + updateMode() + } + } // Container views @IBOutlet private weak var partialContainerView: UIView! @@ -49,6 +53,12 @@ class ThreadRoomTitleView: RoomTitleView { return fullCloseButton } + override var mxRoom: MXRoom! { + didSet { + updateMode() + } + } + override class func nib() -> UINib! { return UINib(nibName: String(describing: self), bundle: .main) @@ -77,7 +87,6 @@ class ThreadRoomTitleView: RoomTitleView { super.awakeFromNib() update(theme: ThemeService.shared().theme) - update() } override func didMoveToSuperview() { @@ -91,6 +100,22 @@ class ThreadRoomTitleView: RoomTitleView { } } + private func updateMode() { + // ensure both mxRoom and threadId are set + guard let room = mxRoom, + let threadId = threadId else { + return + } + + if room.mxSession.threadingService.thread(withId: threadId) == nil { + // thread not created yet + mode = .partial + } else { + // thread created before + mode = .full + } + } + private func update() { switch mode { case .partial: diff --git a/Riot/Modules/Thread/ThreadViewController.swift b/Riot/Modules/Thread/ThreadViewController.swift index bc90734a3..721066972 100644 --- a/Riot/Modules/Thread/ThreadViewController.swift +++ b/Riot/Modules/Thread/ThreadViewController.swift @@ -32,10 +32,8 @@ class ThreadViewController: RoomViewController { return UINib(nibName: String(describing: RoomViewController.self), bundle: .main) } - override func viewDidLoad() { - super.viewDidLoad() - - self.setRoomTitleViewClass(ThreadRoomTitleView.self) + override func setRoomTitleViewClass(_ roomTitleViewClass: AnyClass!) { + super.setRoomTitleViewClass(ThreadRoomTitleView.self) guard let threadTitleView = self.titleView as? ThreadRoomTitleView else { return @@ -44,8 +42,4 @@ class ThreadViewController: RoomViewController { threadTitleView.threadId = threadId } - override func setRoomTitleViewClass(_ roomTitleViewClass: AnyClass!) { - super.setRoomTitleViewClass(ThreadRoomTitleView.self) - } - } From 0008b12d7128b18fa66b89b8d19142c517124a77 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Thu, 11 Nov 2021 12:47:54 +0300 Subject: [PATCH 19/87] Disable threads by default --- Riot/Managers/Settings/RiotSettings.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift index 50b330f87..5a4e50d60 100644 --- a/Riot/Managers/Settings/RiotSettings.swift +++ b/Riot/Managers/Settings/RiotSettings.swift @@ -118,7 +118,7 @@ final class RiotSettings: NSObject { var enableRingingForGroupCalls /// Indicates if threads enabled in the timeline. - @UserDefault(key: "enableThreads", defaultValue: true, storage: defaults) + @UserDefault(key: "enableThreads", defaultValue: false, storage: defaults) var enableThreads // MARK: Calls From 380a874ccbd6638b39c76f52f4498dd9b1525b34 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Thu, 11 Nov 2021 15:00:34 +0300 Subject: [PATCH 20/87] Allow custom sizing when the presented vc is in a navigation stack --- .../CustomSizedPresentationController.swift | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/Riot/Modules/Common/Presentation/CustomSizedPresentationController.swift b/Riot/Modules/Common/Presentation/CustomSizedPresentationController.swift index ce161ecec..554705d73 100644 --- a/Riot/Modules/Common/Presentation/CustomSizedPresentationController.swift +++ b/Riot/Modules/Common/Presentation/CustomSizedPresentationController.swift @@ -180,7 +180,13 @@ class CustomSizedPresentationController: UIPresentationController { } // return value from presentable if implemented - if let presentable = presentedViewController as? CustomSizedPresentable, let customSize = presentable.customSize?(withParentContainerSize: parentSize) { + if let presentable = presentedViewController as? CustomSizedPresentable, + let customSize = presentable.customSize?(withParentContainerSize: parentSize) { + return customSize + } + if let navController = presentedViewController as? UINavigationController, + let presentable = navController.viewControllers.first(where: { $0 is CustomSizedPresentable }) as? CustomSizedPresentable, + let customSize = presentable.customSize?(withParentContainerSize: parentSize) { return customSize } @@ -197,7 +203,13 @@ class CustomSizedPresentationController: UIPresentationController { withParentContainerSize: containerView.bounds.size) // use origin value from presentable if implemented - if let presentable = presentedViewController as? CustomSizedPresentable, let origin = presentable.position?(withParentContainerSize: containerView.bounds.size) { + if let presentable = presentedViewController as? CustomSizedPresentable, + let origin = presentable.position?(withParentContainerSize: containerView.bounds.size) { + return CGRect(origin: origin, size: size) + } + if let navController = presentedViewController as? UINavigationController, + let presentable = navController.viewControllers.first(where: { $0 is CustomSizedPresentable }) as? CustomSizedPresentable, + let origin = presentable.position?(withParentContainerSize: containerView.bounds.size) { return CGRect(origin: origin, size: size) } From 2d97cc04f24627c65dee0d249e70b26f6ded780e Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Thu, 11 Nov 2021 15:01:44 +0300 Subject: [PATCH 21/87] Allow ThreadViewController to be presented with a custom size --- .../Modules/Thread/ThreadViewController.swift | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/Riot/Modules/Thread/ThreadViewController.swift b/Riot/Modules/Thread/ThreadViewController.swift index 721066972..a6df5cc6a 100644 --- a/Riot/Modules/Thread/ThreadViewController.swift +++ b/Riot/Modules/Thread/ThreadViewController.swift @@ -18,6 +18,13 @@ import Foundation class ThreadViewController: RoomViewController { + // MARK: Private + + private enum Constants { + static let sizeOniPad: CGSize = CGSize(width: 375, height: 667) + static let additionalTopInset: CGFloat = 20 + } + private(set) var threadId: String! class func instantiate(withThreadId threadId: String, @@ -42,4 +49,37 @@ class ThreadViewController: RoomViewController { threadTitleView.threadId = threadId } + private func topSafeAreaInset() -> CGFloat { + guard let window = UIApplication.shared.keyWindow else { + return Constants.additionalTopInset + } + + return window.safeAreaInsets.top + Constants.additionalTopInset + } + +} + +// MARK: - CustomSizedPresentable + +extension ThreadViewController: CustomSizedPresentable { + + func customSize(withParentContainerSize containerSize: CGSize) -> CGSize { + if UIDevice.current.isPhone { + return CGSize(width: containerSize.width, + height: containerSize.height - topSafeAreaInset()) + } + return Constants.sizeOniPad + } + + func position(withParentContainerSize containerSize: CGSize) -> CGPoint { + let mySize = customSize(withParentContainerSize: containerSize) + + if UIDevice.current.isPhone { + return CGPoint(x: 0, y: topSafeAreaInset()) + } + + return CGPoint(x: (containerSize.width - mySize.width)/2, + y: (containerSize.height - mySize.height)/2) + } + } From 5da959779b6faee5c6873cb402c57b0e901268a7 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Thu, 11 Nov 2021 15:02:01 +0300 Subject: [PATCH 22/87] Use full title mode for now --- Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.swift b/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.swift index 592071dcf..c7c1bff49 100644 --- a/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.swift +++ b/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.swift @@ -110,6 +110,9 @@ class ThreadRoomTitleView: RoomTitleView { if room.mxSession.threadingService.thread(withId: threadId) == nil { // thread not created yet mode = .partial + // use full mode for every case for now + // TODO: Fix in future + mode = .full } else { // thread created before mode = .full From 136bb7eb9e061d39895781b20ee8a958a5f5f8ee Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Thu, 11 Nov 2021 15:17:01 +0300 Subject: [PATCH 23/87] Present threads with custom sized presentable --- .../Room/RoomCoordinatorBridgePresenter.swift | 16 ++++++++++++++++ Riot/Modules/Room/RoomViewController.m | 5 ++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift b/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift index 9616969fa..6184026a2 100644 --- a/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift +++ b/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift @@ -86,6 +86,22 @@ final class RoomCoordinatorBridgePresenter: NSObject { func present(from viewController: UIViewController, animated: Bool) { + if bridgeParameters.threadId != nil { + let coordinator = self.createRoomCoordinator() + coordinator.delegate = self + coordinator.start() + let presentable = coordinator.toPresentable() + + let presentationController = CustomSizedPresentationController(presentedViewController: presentable, + presenting: viewController) + presentationController.dismissOnBackgroundTap = false + presentationController.cornerRadius = 12 + presentable.transitioningDelegate = presentationController + viewController.present(presentable, animated: animated, completion: nil) + + self.coordinator = coordinator + return + } let coordinator = self.createRoomCoordinator() coordinator.delegate = self let presentable = coordinator.toPresentable() diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index d92628e2f..41a178659 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -6344,9 +6344,8 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; threadId:threadId displayConfiguration:configuration previewData:nil]; - RoomCoordinatorBridgePresenter *presenter = [[RoomCoordinatorBridgePresenter alloc] initWithParameters:parameters]; - self.threadBridgePresenter = presenter; - [presenter presentFrom:self animated:YES]; + self.threadBridgePresenter = [[RoomCoordinatorBridgePresenter alloc] initWithParameters:parameters]; + [self.threadBridgePresenter presentFrom:self animated:YES]; } #pragma mark - RoomContextualMenuViewControllerDelegate From b1dca20e9be3fcbf6f524160e3f7b2a551895a38 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Thu, 11 Nov 2021 15:25:21 +0300 Subject: [PATCH 24/87] Add a to-do for a bad code --- Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.swift b/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.swift index c7c1bff49..83be5905f 100644 --- a/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.swift +++ b/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.swift @@ -92,6 +92,7 @@ class ThreadRoomTitleView: RoomTitleView { override func didMoveToSuperview() { super.didMoveToSuperview() + // TODO: Find a way to handle this properly if let superview = superview?.superview { NSLayoutConstraint.activate([ self.leadingAnchor.constraint(equalTo: superview.leadingAnchor), From 0338452777185941b7da47559f7c1a613ae4de59 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Thu, 11 Nov 2021 22:26:30 +0300 Subject: [PATCH 25/87] Add new threadId parameters --- .../Shared/ForwardingShareItemSender.swift | 2 +- RiotShareExtension/Sources/ShareItemSender.m | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/RiotShareExtension/Shared/ForwardingShareItemSender.swift b/RiotShareExtension/Shared/ForwardingShareItemSender.swift index 84b866b27..3909812db 100644 --- a/RiotShareExtension/Shared/ForwardingShareItemSender.swift +++ b/RiotShareExtension/Shared/ForwardingShareItemSender.swift @@ -51,7 +51,7 @@ class ForwardingShareItemSender: NSObject, ShareItemSenderProtocol { dispatchGroup.enter() var localEcho: MXEvent? - room.sendMessage(withContent: event.content, localEcho: &localEcho) { result in + room.sendMessage(withContent: event.content, threadId: nil, localEcho: &localEcho) { result in switch result { case .failure(let innerError): errors.append(innerError) diff --git a/RiotShareExtension/Sources/ShareItemSender.m b/RiotShareExtension/Sources/ShareItemSender.m index e1cd1add6..fdc213033 100644 --- a/RiotShareExtension/Sources/ShareItemSender.m +++ b/RiotShareExtension/Sources/ShareItemSender.m @@ -525,7 +525,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) dispatch_group_t dispatchGroup = dispatch_group_create(); for (MXRoom *room in rooms) { dispatch_group_enter(dispatchGroup); - [room sendTextMessage:text success:^(NSString *eventId) { + [room sendTextMessage:text threadId:nil success:^(NSString *eventId) { dispatch_group_leave(dispatchGroup); } failure:^(NSError *innerError) { MXLogError(@"[ShareItemSender] sendTextMessage failed with error %@", error); @@ -565,7 +565,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) dispatch_group_t dispatchGroup = dispatch_group_create(); for (MXRoom *room in rooms) { dispatch_group_enter(dispatchGroup); - [room sendFile:fileUrl mimeType:mimeType localEcho:nil success:^(NSString *eventId) { + [room sendFile:fileUrl mimeType:mimeType threadId:nil localEcho:nil success:^(NSString *eventId) { dispatch_group_leave(dispatchGroup); } failure:^(NSError *innerError) { MXLogError(@"[ShareItemSender] sendFile failed with error %@", innerError); @@ -616,7 +616,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) dispatch_group_t dispatchGroup = dispatch_group_create(); for (MXRoom *room in rooms) { dispatch_group_enter(dispatchGroup); - [room sendVideoAsset:videoAsset withThumbnail:videoThumbnail localEcho:nil success:^(NSString *eventId) { + [room sendVideoAsset:videoAsset withThumbnail:videoThumbnail threadId:nil localEcho:nil success:^(NSString *eventId) { dispatch_group_leave(dispatchGroup); } failure:^(NSError *innerError) { MXLogError(@"[ShareManager] Failed sending video with error %@", innerError); @@ -702,7 +702,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) dispatch_group_t dispatchGroup = dispatch_group_create(); for (MXRoom *room in rooms) { dispatch_group_enter(dispatchGroup); - [room sendVoiceMessage:fileUrl mimeType:nil duration:0.0 samples:nil localEcho:nil success:^(NSString *eventId) { + [room sendVoiceMessage:fileUrl mimeType:nil duration:0.0 samples:nil threadId:nil localEcho:nil success:^(NSString *eventId) { dispatch_group_leave(dispatchGroup); } failure:^(NSError *innerError) { MXLogError(@"[ShareItemSender] sendVoiceMessage failed with error %@", error); @@ -867,7 +867,7 @@ typedef NS_ENUM(NSInteger, ImageCompressionMode) } dispatch_group_enter(dispatchGroup); - [room sendImage:finalImageData withImageSize:imageSize mimeType:mimeType andThumbnail:thumbnail localEcho:nil success:^(NSString *eventId) { + [room sendImage:finalImageData withImageSize:imageSize mimeType:mimeType andThumbnail:thumbnail threadId:nil localEcho:nil success:^(NSString *eventId) { dispatch_group_leave(dispatchGroup); } failure:^(NSError *innerError) { MXLogError(@"[ShareManager] sendImage failed with error %@", error); From c2b912cd11b80137c51e7fb923cc9f6cff82b7f7 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Thu, 11 Nov 2021 22:28:29 +0300 Subject: [PATCH 26/87] Reload data source when a new thread created --- Riot/Modules/Room/DataSources/RoomDataSource.m | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m index 0ad4d2b42..873d74fc0 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.m +++ b/Riot/Modules/Room/DataSources/RoomDataSource.m @@ -93,6 +93,11 @@ const CGFloat kTypingCellHeight = 24; }]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(newThreadCreated:) + name:MXThreadingService.newThreadCreated + object:nil]; + [self registerKeyVerificationRequestNotification]; [self registerKeyVerificationTransactionNotification]; [self registerTrustLevelDidChangeNotifications]; @@ -1119,6 +1124,13 @@ const CGFloat kTypingCellHeight = 24; cell.attachmentView.accessibilityLabel = nil; } +#pragma mark - Threads + +- (void)newThreadCreated:(NSNotification *)notification +{ + [self reload]; +} + #pragma mark - BubbleReactionsViewModelDelegate - (void)bubbleReactionsViewModel:(BubbleReactionsViewModel *)viewModel didAddReaction:(MXReactionCount *)reactionCount forEventId:(NSString *)eventId From 72f86e8a15a2b31f5374e54597012d48ed46985c Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Thu, 11 Nov 2021 22:38:41 +0300 Subject: [PATCH 27/87] Reload data source only after initialization --- Riot/Modules/Room/DataSources/RoomDataSource.m | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m index 873d74fc0..19bc4fcf2 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.m +++ b/Riot/Modules/Room/DataSources/RoomDataSource.m @@ -1128,7 +1128,15 @@ const CGFloat kTypingCellHeight = 24; - (void)newThreadCreated:(NSNotification *)notification { - [self reload]; + NSUInteger count = 0; + @synchronized (bubbles) + { + count = bubbles.count; + } + if (count > 0) + { + [self reload]; + } } #pragma mark - BubbleReactionsViewModelDelegate From b35a9024ba24abc8219402f08feb7e6fdaedee55 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Fri, 12 Nov 2021 17:24:58 +0300 Subject: [PATCH 28/87] Add eventId parameter to delegates --- Riot/Modules/Room/RoomCoordinator.swift | 4 ++-- Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift | 8 +++++--- Riot/Modules/Room/RoomCoordinatorProtocol.swift | 2 +- Riot/Modules/Room/RoomViewController.h | 4 +++- Riot/Modules/Room/RoomViewController.m | 2 +- Riot/Modules/TabBar/TabBarCoordinator.swift | 9 +++++---- 6 files changed, 17 insertions(+), 12 deletions(-) diff --git a/Riot/Modules/Room/RoomCoordinator.swift b/Riot/Modules/Room/RoomCoordinator.swift index 6115fd3f8..44273f246 100644 --- a/Riot/Modules/Room/RoomCoordinator.swift +++ b/Riot/Modules/Room/RoomCoordinator.swift @@ -253,8 +253,8 @@ extension RoomCoordinator: UIAdaptivePresentationControllerDelegate { // MARK: - RoomViewControllerDelegate extension RoomCoordinator: RoomViewControllerDelegate { - func roomViewController(_ roomViewController: RoomViewController, showRoomWithId roomID: String) { - self.delegate?.roomCoordinator(self, didSelectRoomWithId: roomID) + func roomViewController(_ roomViewController: RoomViewController, showRoomWithId roomID: String, eventId eventID: String?) { + self.delegate?.roomCoordinator(self, didSelectRoomWithId: roomID, eventId: eventID) } func roomViewController(_ roomViewController: RoomViewController, showMemberDetails roomMember: MXRoomMember) { diff --git a/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift b/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift index 6184026a2..60d99f8d6 100644 --- a/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift +++ b/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift @@ -18,7 +18,9 @@ import Foundation @objc protocol RoomCoordinatorBridgePresenterDelegate { func roomCoordinatorBridgePresenterDidLeaveRoom(_ bridgePresenter: RoomCoordinatorBridgePresenter) func roomCoordinatorBridgePresenterDidCancelRoomPreview(_ bridgePresenter: RoomCoordinatorBridgePresenter) - func roomCoordinatorBridgePresenter(_ bridgePresenter: RoomCoordinatorBridgePresenter, didSelectRoomWithId roomId: String) + func roomCoordinatorBridgePresenter(_ bridgePresenter: RoomCoordinatorBridgePresenter, + didSelectRoomWithId roomId: String, + eventId: String?) func roomCoordinatorBridgePresenterDidDismissInteractively(_ bridgePresenter: RoomCoordinatorBridgePresenter) } @@ -158,8 +160,8 @@ final class RoomCoordinatorBridgePresenter: NSObject { // MARK: - RoomNotificationSettingsCoordinatorDelegate extension RoomCoordinatorBridgePresenter: RoomCoordinatorDelegate { - func roomCoordinator(_ coordinator: RoomCoordinatorProtocol, didSelectRoomWithId roomId: String) { - self.delegate?.roomCoordinatorBridgePresenter(self, didSelectRoomWithId: roomId) + func roomCoordinator(_ coordinator: RoomCoordinatorProtocol, didSelectRoomWithId roomId: String, eventId: String?) { + self.delegate?.roomCoordinatorBridgePresenter(self, didSelectRoomWithId: roomId, eventId: eventId) } func roomCoordinatorDidLeaveRoom(_ coordinator: RoomCoordinatorProtocol) { diff --git a/Riot/Modules/Room/RoomCoordinatorProtocol.swift b/Riot/Modules/Room/RoomCoordinatorProtocol.swift index 1c30f02ec..9fe774ce6 100644 --- a/Riot/Modules/Room/RoomCoordinatorProtocol.swift +++ b/Riot/Modules/Room/RoomCoordinatorProtocol.swift @@ -21,7 +21,7 @@ import Foundation protocol RoomCoordinatorDelegate: AnyObject { func roomCoordinatorDidLeaveRoom(_ coordinator: RoomCoordinatorProtocol) func roomCoordinatorDidCancelRoomPreview(_ coordinator: RoomCoordinatorProtocol) - func roomCoordinator(_ coordinator: RoomCoordinatorProtocol, didSelectRoomWithId roomId: String) + func roomCoordinator(_ coordinator: RoomCoordinatorProtocol, didSelectRoomWithId roomId: String, eventId: String?) func roomCoordinatorDidDismissInteractively(_ coordinator: RoomCoordinatorProtocol) } diff --git a/Riot/Modules/Room/RoomViewController.h b/Riot/Modules/Room/RoomViewController.h index b182dbddf..aec84d4bc 100644 --- a/Riot/Modules/Room/RoomViewController.h +++ b/Riot/Modules/Room/RoomViewController.h @@ -137,9 +137,11 @@ extern NSNotificationName const RoomGroupCallTileTappedNotification; @param roomViewController the `RoomViewController` instance. @param roomID the selected roomId + @param eventID the selected eventId */ - (void)roomViewController:(RoomViewController *)roomViewController - showRoomWithId:(NSString *)roomID; + showRoomWithId:(NSString *)roomID + eventId:(nullable NSString *)eventID; /** Tells the delegate that the user wants to start a direct chat with a user. diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 5f9f73bfb..791066af8 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -2192,7 +2192,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; { if (self.delegate) { - [self.delegate roomViewController:self showRoomWithId:roomId]; + [self.delegate roomViewController:self showRoomWithId:roomId eventId:nil]; } else { diff --git a/Riot/Modules/TabBar/TabBarCoordinator.swift b/Riot/Modules/TabBar/TabBarCoordinator.swift index 63bf354a2..fafea903c 100644 --- a/Riot/Modules/TabBar/TabBarCoordinator.swift +++ b/Riot/Modules/TabBar/TabBarCoordinator.swift @@ -381,13 +381,13 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { } } - private func showRoom(withId roomId: String) { + private func showRoom(withId roomId: String, eventId: String? = nil) { guard let matrixSession = self.parameters.userSessionsService.mainUserSession?.matrixSession else { return } - self.showRoom(with: roomId, eventId: nil, matrixSession: matrixSession) + self.showRoom(with: roomId, eventId: eventId, matrixSession: matrixSession) } private func showRoom(withNavigationParameters roomNavigationParameters: RoomNavigationParameters, completion: (() -> Void)?) { @@ -596,9 +596,10 @@ extension TabBarCoordinator: RoomCoordinatorDelegate { self.navigationRouter.popModule(animated: true) } - func roomCoordinator(_ coordinator: RoomCoordinatorProtocol, didSelectRoomWithId roomId: String) { - self.showRoom(withId: roomId) + func roomCoordinator(_ coordinator: RoomCoordinatorProtocol, didSelectRoomWithId roomId: String, eventId: String?) { + self.showRoom(withId: roomId, eventId: eventId) } + } // MARK: - UIGestureRecognizerDelegate From 00bb071504852e9f22cb6a6ba00f7f3e1c612b49 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Fri, 12 Nov 2021 17:25:58 +0300 Subject: [PATCH 29/87] Add new action for the thread root event in thread view --- Riot/Assets/en.lproj/Vector.strings | 1 + Riot/Generated/Strings.swift | 4 ++++ Riot/Modules/Room/RoomViewController.m | 14 ++++++++++++++ 3 files changed, 19 insertions(+) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 59dedb1c9..c3a9b827a 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -362,6 +362,7 @@ Tap the + to start adding people."; "room_event_action_more" = "More"; "room_event_action_share" = "Share"; "room_event_action_forward" = "Forward"; +"room_event_action_view_in_room" = "View in room"; "room_event_action_permalink" = "Permalink"; "room_event_action_view_source" = "View Source"; "room_event_action_view_decrypted_source" = "View Decrypted Source"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index d4069d892..a7b4e8106 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -2991,6 +2991,10 @@ public class VectorL10n: NSObject { public static var roomEventActionViewEncryption: String { return VectorL10n.tr("Vector", "room_event_action_view_encryption") } + /// View in room + public static var roomEventActionViewInRoom: String { + return VectorL10n.tr("Vector", "room_event_action_view_in_room") + } /// View Source public static var roomEventActionViewSource: String { return VectorL10n.tr("Vector", "room_event_action_view_source") diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 791066af8..98f17eea3 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -3259,6 +3259,20 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; }]]; } + if (self.roomDataSource.threadId && [selectedEvent.eventId isEqualToString:self.roomDataSource.threadId]) + { + // if in the thread and selected event is the root event + // add "View in room" action + [currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionViewInRoom] + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { + MXStrongifyAndReturnIfNil(self); + [self.delegate roomViewController:self + showRoomWithId:self.roomDataSource.roomId + eventId:selectedEvent.eventId]; + }]]; + } + if (!isJitsiCallEvent) { [currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionQuote] From cdc1f4b4128d49fac35d1b35c35e82491e5a3302 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Fri, 12 Nov 2021 17:26:49 +0300 Subject: [PATCH 30/87] Implement bridge presenter delegate for opened thread --- Riot/Modules/Room/RoomViewController.m | 37 +++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 98f17eea3..3d43fbff6 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -140,7 +140,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; @interface RoomViewController () + RoomDataSourceDelegate, RoomCreationModalCoordinatorBridgePresenterDelegate, RoomInfoCoordinatorBridgePresenterDelegate, DialpadViewControllerDelegate, RemoveJitsiWidgetViewDelegate, VoiceMessageControllerDelegate, SpaceDetailPresenterDelegate, UserSuggestionCoordinatorBridgeDelegate, RoomCoordinatorBridgePresenterDelegate> { // The preview header @@ -6276,6 +6276,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; displayConfiguration:configuration previewData:nil]; self.threadBridgePresenter = [[RoomCoordinatorBridgePresenter alloc] initWithParameters:parameters]; + self.threadBridgePresenter.delegate = self; [self.threadBridgePresenter presentFrom:self animated:YES]; } @@ -6678,4 +6679,38 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [self mention:member]; } +#pragma mark - RoomCoordinatorBridgePresenterDelegate + +- (void)roomCoordinatorBridgePresenterDidLeaveRoom:(RoomCoordinatorBridgePresenter *)bridgePresenter +{ + +} + +- (void)roomCoordinatorBridgePresenterDidCancelRoomPreview:(RoomCoordinatorBridgePresenter *)bridgePresenter +{ + +} + +- (void)roomCoordinatorBridgePresenter:(RoomCoordinatorBridgePresenter *)bridgePresenter + didSelectRoomWithId:(NSString *)roomId + eventId:(NSString*)eventId +{ + if (bridgePresenter == self.threadBridgePresenter && [roomId isEqualToString:self.roomDataSource.roomId]) + { + // thread view wants to highlight an event in the timeline + // dismiss thread view first + [self.threadBridgePresenter dismissWithAnimated:YES completion:^{ + self->customizedRoomDataSource.selectedEventId = eventId; + }]; + } +} + +- (void)roomCoordinatorBridgePresenterDidDismissInteractively:(RoomCoordinatorBridgePresenter *)bridgePresenter +{ + if (bridgePresenter == self.threadBridgePresenter) + { + self.threadBridgePresenter = nil; + } +} + @end From 78880684390871fc55a765997a86572bc2b42278 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Fri, 12 Nov 2021 17:30:31 +0300 Subject: [PATCH 31/87] Hide thread context menu in the thread --- Riot/Modules/Room/RoomViewController.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 5f9f73bfb..a684ed105 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -5903,9 +5903,9 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [items addObject:[self copyMenuItemWithEvent:event andCell:cell]]; [items addObject:[self replyMenuItemWithEvent:event]]; - if (RiotSettings.shared.enableThreads && !event.threadIdentifier) + if (RiotSettings.shared.enableThreads && !self.roomDataSource.threadId && !event.threadIdentifier) { - // add "reply in thread" option only if not a thread already + // add "reply in thread" option only if not in thread already [items addObject:[self replyInThreadMenuItemWithEvent:event]]; } [items addObject:[self editMenuItemWithEvent:event]]; From c046a7779bab818f031066d4b32797d60d1c79c6 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Mon, 15 Nov 2021 13:50:08 +0300 Subject: [PATCH 32/87] Highlight thread root event in the timeline --- .../Modules/Room/DataSources/RoomDataSource.h | 6 +++ .../Modules/Room/DataSources/RoomDataSource.m | 5 +- Riot/Modules/Room/RoomViewController.m | 46 ++++++++++++++++++- 3 files changed, 53 insertions(+), 4 deletions(-) diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.h b/Riot/Modules/Room/DataSources/RoomDataSource.h index 1283e0ffb..70dbf9021 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.h +++ b/Riot/Modules/Room/DataSources/RoomDataSource.h @@ -55,6 +55,12 @@ */ @property(nonatomic, nullable) NSArray *currentTypingUsers; +/** + Identifier of the event to be highlighted. Default is nil. + Data source owner should reload the view itself to reflect changes, and nullify the parameter afterwards when it doesn't highlight the event anymore. + */ +@property (nonatomic, nullable) NSString *highlightedEventId; + /** Check if there is an active jitsi widget in the room and return it. diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m index 19bc4fcf2..3a06fee1c 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.m +++ b/Riot/Modules/Room/DataSources/RoomDataSource.m @@ -774,14 +774,15 @@ const CGFloat kTypingCellHeight = 24; } // Manage initial event (case of permalink or search result) - if (self.timeline.initialEventId && self.markTimelineInitialEvent) + if ((self.timeline.initialEventId && self.markTimelineInitialEvent) || self.highlightedEventId) { // Check if the cell contains this initial event for (NSUInteger index = 0; index < bubbleComponents.count; index++) { MXKRoomBubbleComponent *component = bubbleComponents[index]; - if ([component.event.eventId isEqualToString:self.timeline.initialEventId]) + if ([component.event.eventId isEqualToString:self.timeline.initialEventId] + || [component.event.eventId isEqualToString:self.highlightedEventId]) { // If yes, mark the event [bubbleCell markComponent:index]; diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 3d43fbff6..10385b573 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -3935,6 +3935,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; customizedRoomDataSource.showBubbleDateTimeOnSelection = YES; customizedRoomDataSource.selectedEventId = nil; + customizedRoomDataSource.highlightedEventId = nil; [self restoreTextMessageBeforeEditing]; @@ -4430,6 +4431,25 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; { [super scrollViewWillBeginDragging:scrollView]; } + + // if data source is highlighting an event, dismiss the highlight when user dragges the table view + if (customizedRoomDataSource.highlightedEventId) + { + NSInteger row = [self.roomDataSource indexOfCellDataWithEventId:customizedRoomDataSource.highlightedEventId]; + if (row == NSNotFound) + { + customizedRoomDataSource.highlightedEventId = nil; + return; + } + + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:0]; + if ([[self.bubblesTableView indexPathsForVisibleRows] containsObject:indexPath]) + { + customizedRoomDataSource.highlightedEventId = nil; + [self.bubblesTableView reloadRowsAtIndexPaths:@[indexPath] + withRowAnimation:UITableViewRowAnimationAutomatic]; + } + } } - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate @@ -6695,12 +6715,34 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; didSelectRoomWithId:(NSString *)roomId eventId:(NSString*)eventId { - if (bridgePresenter == self.threadBridgePresenter && [roomId isEqualToString:self.roomDataSource.roomId]) + if (bridgePresenter == self.threadBridgePresenter && [roomId isEqualToString:self.roomDataSource.roomId] && eventId) { // thread view wants to highlight an event in the timeline // dismiss thread view first + MXWeakify(self); [self.threadBridgePresenter dismissWithAnimated:YES completion:^{ - self->customizedRoomDataSource.selectedEventId = eventId; + MXStrongifyAndReturnIfNil(self); + + NSInteger row = [self.roomDataSource indexOfCellDataWithEventId:eventId]; + if (row == NSNotFound) + { + return; + } + + self->customizedRoomDataSource.highlightedEventId = eventId; + + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:0]; + if ([[self.bubblesTableView indexPathsForVisibleRows] containsObject:indexPath]) + { + [self.bubblesTableView reloadRowsAtIndexPaths:@[indexPath] + withRowAnimation:UITableViewRowAnimationAutomatic]; + } + else if ([self.bubblesTableView vc_hasIndexPath:indexPath]) + { + [self.bubblesTableView scrollToRowAtIndexPath:indexPath + atScrollPosition:UITableViewScrollPositionMiddle + animated:YES]; + } }]; } } From 3fc667658b037e24fc8ab86075e39b17e85cb15f Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Mon, 15 Nov 2021 14:00:36 +0300 Subject: [PATCH 33/87] Add changelog --- changelog.d/5117.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/5117.feature diff --git a/changelog.d/5117.feature b/changelog.d/5117.feature new file mode 100644 index 000000000..36eff840f --- /dev/null +++ b/changelog.d/5117.feature @@ -0,0 +1 @@ +Threads: Add `View in room` action to the thread root event. From 16f2cd9ad7066f02f719652e6474ea6f08c1b047 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Mon, 15 Nov 2021 17:44:21 +0300 Subject: [PATCH 34/87] Move copy action into the more action sheet if threading enabled for an event --- Riot/Modules/Room/RoomViewController.m | 163 +++++++++++++++---------- 1 file changed, 98 insertions(+), 65 deletions(-) diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index a684ed105..98425c5f8 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -3189,6 +3189,22 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; MXWeakify(self); currentAlert = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet]; + BOOL showThreadOption = RiotSettings.shared.enableThreads + && !self.roomDataSource.threadId + && !selectedEvent.threadIdentifier; + if (showThreadOption && [self canCopyEvent:selectedEvent andCell:cell]) + { + [currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionCopy] + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { + MXStrongifyAndReturnIfNil(self); + + [self cancelEventSelection]; + + [self copyEvent:selectedEvent inCell:cell]; + }]]; + } + // Add actions for a failed event if (selectedEvent.sentState == MXEventSentStateFailed) { @@ -5898,14 +5914,18 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; BOOL showMoreOption = (event.isState && RiotSettings.shared.roomContextualMenuShowMoreOptionForStates) || (!event.isState && RiotSettings.shared.roomContextualMenuShowMoreOptionForMessages); + BOOL showThreadOption = RiotSettings.shared.enableThreads && !self.roomDataSource.threadId && !event.threadIdentifier; NSMutableArray *items = [NSMutableArray arrayWithCapacity:5]; - [items addObject:[self copyMenuItemWithEvent:event andCell:cell]]; - [items addObject:[self replyMenuItemWithEvent:event]]; - if (RiotSettings.shared.enableThreads && !self.roomDataSource.threadId && !event.threadIdentifier) + if (!showThreadOption) { - // add "reply in thread" option only if not in thread already + [items addObject:[self copyMenuItemWithEvent:event andCell:cell]]; + } + [items addObject:[self replyMenuItemWithEvent:event]]; + if (showThreadOption) + { + // add "Thread" option only if not already in a thread [items addObject:[self replyInThreadMenuItemWithEvent:event]]; } [items addObject:[self editMenuItemWithEvent:event]]; @@ -6088,20 +6108,33 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; } - (RoomContextualMenuItem *)copyMenuItemWithEvent:(MXEvent*)event andCell:(id)cell +{ + MXWeakify(self); + + RoomContextualMenuItem *copyMenuItem = [[RoomContextualMenuItem alloc] initWithMenuAction:RoomContextualMenuActionCopy]; + copyMenuItem.isEnabled = [self canCopyEvent:event andCell:cell]; + copyMenuItem.action = ^{ + MXStrongifyAndReturnIfNil(self); + + [self copyEvent:event inCell:cell]; + }; + + return copyMenuItem; +} + +- (BOOL)canCopyEvent:(MXEvent*)event andCell:(id)cell { MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell *)cell; MXKAttachment *attachment = roomBubbleTableViewCell.bubbleData.attachment; - MXWeakify(self); - - BOOL isCopyActionEnabled = !attachment || attachment.type != MXKAttachmentTypeSticker; + BOOL result = !attachment || attachment.type != MXKAttachmentTypeSticker; if (attachment && !BuildSettings.messageDetailsAllowCopyMedia) { - isCopyActionEnabled = NO; + result = NO; } - if (isCopyActionEnabled) + if (result) { switch (event.eventType) { case MXEventTypeRoomMessage: @@ -6110,7 +6143,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; if ([messageType isEqualToString:kMXMessageTypeKeyVerificationRequest]) { - isCopyActionEnabled = NO; + result = NO; } break; } @@ -6120,7 +6153,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; case MXEventTypeKeyVerificationMac: case MXEventTypeKeyVerificationDone: case MXEventTypeKeyVerificationCancel: - isCopyActionEnabled = NO; + result = NO; break; case MXEventTypeCustom: if ([event.type isEqualToString:kWidgetMatrixEventTypeString] @@ -6130,7 +6163,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; if ([widget.type isEqualToString:kWidgetTypeJitsiV1] || [widget.type isEqualToString:kWidgetTypeJitsiV2]) { - isCopyActionEnabled = NO; + result = NO; } } default: @@ -6138,60 +6171,60 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; } } - RoomContextualMenuItem *copyMenuItem = [[RoomContextualMenuItem alloc] initWithMenuAction:RoomContextualMenuActionCopy]; - copyMenuItem.isEnabled = isCopyActionEnabled; - copyMenuItem.action = ^{ - MXStrongifyAndReturnIfNil(self); - - if (!attachment) - { - NSArray *components = roomBubbleTableViewCell.bubbleData.bubbleComponents; - MXKRoomBubbleComponent *selectedComponent; - for (selectedComponent in components) - { - if ([selectedComponent.event.eventId isEqualToString:event.eventId]) - { - break; - } - selectedComponent = nil; - } - NSString *textMessage = selectedComponent.textMessage; - - if (textMessage) - { - MXKPasteboardManager.shared.pasteboard.string = textMessage; - } - else - { - MXLogDebug(@"[RoomViewController] Contextual menu copy failed. Text is nil for room id/event id: %@/%@", selectedComponent.event.roomId, selectedComponent.event.eventId); - } - - [self hideContextualMenuAnimated:YES]; - } - else if (attachment.type != MXKAttachmentTypeSticker) - { - [self hideContextualMenuAnimated:YES completion:^{ - [self startActivityIndicator]; - - [attachment copy:^{ - - [self stopActivityIndicator]; - - } failure:^(NSError *error) { - - [self stopActivityIndicator]; - - //Alert user - [self showError:error]; - }]; - - // Start animation in case of download during attachment preparing - [roomBubbleTableViewCell startProgressUI]; - }]; - } - }; + return result; +} + +- (void)copyEvent:(MXEvent*)event inCell:(id)cell +{ + MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell *)cell; + MXKAttachment *attachment = roomBubbleTableViewCell.bubbleData.attachment; - return copyMenuItem; + if (!attachment) + { + NSArray *components = roomBubbleTableViewCell.bubbleData.bubbleComponents; + MXKRoomBubbleComponent *selectedComponent; + for (selectedComponent in components) + { + if ([selectedComponent.event.eventId isEqualToString:event.eventId]) + { + break; + } + selectedComponent = nil; + } + NSString *textMessage = selectedComponent.textMessage; + + if (textMessage) + { + MXKPasteboardManager.shared.pasteboard.string = textMessage; + } + else + { + MXLogDebug(@"[RoomViewController] Contextual menu copy failed. Text is nil for room id/event id: %@/%@", selectedComponent.event.roomId, selectedComponent.event.eventId); + } + + [self hideContextualMenuAnimated:YES]; + } + else if (attachment.type != MXKAttachmentTypeSticker) + { + [self hideContextualMenuAnimated:YES completion:^{ + [self startActivityIndicator]; + + [attachment copy:^{ + + [self stopActivityIndicator]; + + } failure:^(NSError *error) { + + [self stopActivityIndicator]; + + //Alert user + [self showError:error]; + }]; + + // Start animation in case of download during attachment preparing + [roomBubbleTableViewCell startProgressUI]; + }]; + } } - (RoomContextualMenuItem *)replyMenuItemWithEvent:(MXEvent*)event From d629c7c782569a457b0bdec631bdd1073635ff56 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Tue, 16 Nov 2021 17:14:57 +0300 Subject: [PATCH 35/87] Generate classes --- Riot/Assets/en.lproj/Vector.strings | 1 + Riot/Generated/Storyboards.swift | 5 + Riot/Generated/Strings.swift | 4 + Riot/Modules/Room/RoomViewController.m | 25 +++ .../Thread/ThreadViewController.swift | 0 .../ThreadList/ThreadListCoordinator.swift | 70 +++++++ .../ThreadListCoordinatorParameters.swift | 28 +++ .../ThreadListCoordinatorProtocol.swift | 29 +++ .../ThreadList/ThreadListViewAction.swift | 26 +++ .../ThreadListViewController.storyboard | 96 ++++++++++ .../ThreadList/ThreadListViewController.swift | 180 ++++++++++++++++++ .../ThreadList/ThreadListViewModel.swift | 93 +++++++++ .../ThreadListViewModelProtocol.swift | 39 ++++ .../ThreadList/ThreadListViewState.swift | 27 +++ Riot/Modules/Threads/ThreadsCoordinator.swift | 102 ++++++++++ .../ThreadsCoordinatorBridgePresenter.swift | 139 ++++++++++++++ .../ThreadsCoordinatorParameters.swift | 40 ++++ .../Threads/ThreadsCoordinatorProtocol.swift | 31 +++ 18 files changed, 935 insertions(+) rename Riot/Modules/{ => Threads}/Thread/ThreadViewController.swift (100%) create mode 100644 Riot/Modules/Threads/ThreadList/ThreadListCoordinator.swift create mode 100644 Riot/Modules/Threads/ThreadList/ThreadListCoordinatorParameters.swift create mode 100644 Riot/Modules/Threads/ThreadList/ThreadListCoordinatorProtocol.swift create mode 100644 Riot/Modules/Threads/ThreadList/ThreadListViewAction.swift create mode 100644 Riot/Modules/Threads/ThreadList/ThreadListViewController.storyboard create mode 100644 Riot/Modules/Threads/ThreadList/ThreadListViewController.swift create mode 100644 Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift create mode 100644 Riot/Modules/Threads/ThreadList/ThreadListViewModelProtocol.swift create mode 100644 Riot/Modules/Threads/ThreadList/ThreadListViewState.swift create mode 100644 Riot/Modules/Threads/ThreadsCoordinator.swift create mode 100644 Riot/Modules/Threads/ThreadsCoordinatorBridgePresenter.swift create mode 100644 Riot/Modules/Threads/ThreadsCoordinatorParameters.swift create mode 100644 Riot/Modules/Threads/ThreadsCoordinatorProtocol.swift diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index c3a9b827a..dabfba721 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -409,6 +409,7 @@ Tap the + to start adding people."; "room_accessibility_upload" = "Upload"; "room_accessibility_call" = "Call"; "room_accessibility_video_call" = "Video Call"; +"room_accessibility_threads" = "Threads"; "room_accessibility_hangup" = "Hang up"; "room_place_voice_call" = "Voice call"; "room_open_dialpad" = "Dial pad"; diff --git a/Riot/Generated/Storyboards.swift b/Riot/Generated/Storyboards.swift index 34615dd94..2a92564a0 100644 --- a/Riot/Generated/Storyboards.swift +++ b/Riot/Generated/Storyboards.swift @@ -279,6 +279,11 @@ internal enum StoryboardScene { internal static let initialScene = InitialSceneType(storyboard: TemplateScreenViewController.self) } + internal enum ThreadListViewController: StoryboardType { + internal static let storyboardName = "ThreadListViewController" + + internal static let initialScene = InitialSceneType(storyboard: ThreadListViewController.self) + } internal enum UserVerificationSessionStatusViewController: StoryboardType { internal static let storyboardName = "UserVerificationSessionStatusViewController" diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index a7b4e8106..bb9ac8ac7 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -2451,6 +2451,10 @@ public class VectorL10n: NSObject { public static var roomAccessibilitySearch: String { return VectorL10n.tr("Vector", "room_accessibility_search") } + /// Threads + public static var roomAccessibilityThreads: String { + return VectorL10n.tr("Vector", "room_accessibility_threads") + } /// Upload public static var roomAccessibilityUpload: String { return VectorL10n.tr("Vector", "room_accessibility_upload") diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index ffd7a2c96..64178c3e2 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -246,6 +246,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; @property (nonatomic, strong) RoomInfoCoordinatorBridgePresenter *roomInfoCoordinatorBridgePresenter; @property (nonatomic, strong) RoomCoordinatorBridgePresenter *threadBridgePresenter; @property (nonatomic, strong) CustomSizedPresentationController *customSizedPresentationController; +@property (nonatomic, strong) ThreadsCoordinatorBridgePresenter *threadsCoordinatorBridgePresenter; @property (nonatomic, getter=isActivitiesViewExpanded) BOOL activitiesViewExpanded; @property (nonatomic, getter=isScrollToBottomHidden) BOOL scrollToBottomHidden; @property (nonatomic, getter=isMissedDiscussionsBadgeHidden) BOOL missedDiscussionsBadgeHidden; @@ -1517,6 +1518,17 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; return item; } +- (UIBarButtonItem *)threadListBarButtonItem +{ + UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"room_context_menu_reply_in_thread"] + style:UIBarButtonItemStylePlain + target:self + action:@selector(onThreadListTapped:)]; + item.accessibilityLabel = [VectorL10n roomAccessibilityThreads]; + + return item; +} + - (void)setupRemoveJitsiWidgetRemoveView { if (!self.displayConfiguration.jitsiWidgetRemoverEnabled) @@ -1748,6 +1760,12 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; } [self refreshMissedDiscussionsCount:YES]; + + if (RiotSettings.shared.enableThreads) + { + UIBarButtonItem *itemThreadList = [self threadListBarButtonItem]; + [rightBarButtonItems insertObject:itemThreadList atIndex:0]; + } } self.navigationItem.rightBarButtonItems = rightBarButtonItems; @@ -4322,6 +4340,13 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [self placeCallWithVideo:YES]; } +- (IBAction)onThreadListTapped:(id)sender +{ + self.threadsCoordinatorBridgePresenter = [[ThreadsCoordinatorBridgePresenter alloc] initWithSession:self.mainSession + roomId:self.roomDataSource.roomId]; + [self.threadsCoordinatorBridgePresenter pushFrom:self.navigationController animated:YES]; +} + - (IBAction)onIntegrationsPressed:(id)sender { WidgetPickerViewController *widgetPicker = [[WidgetPickerViewController alloc] initForMXSession:self.roomDataSource.mxSession diff --git a/Riot/Modules/Thread/ThreadViewController.swift b/Riot/Modules/Threads/Thread/ThreadViewController.swift similarity index 100% rename from Riot/Modules/Thread/ThreadViewController.swift rename to Riot/Modules/Threads/Thread/ThreadViewController.swift diff --git a/Riot/Modules/Threads/ThreadList/ThreadListCoordinator.swift b/Riot/Modules/Threads/ThreadList/ThreadListCoordinator.swift new file mode 100644 index 000000000..a672cf12a --- /dev/null +++ b/Riot/Modules/Threads/ThreadList/ThreadListCoordinator.swift @@ -0,0 +1,70 @@ +// File created from ScreenTemplate +// $ createScreen.sh Threads/ThreadList ThreadList +/* + 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 Foundation +import UIKit + +final class ThreadListCoordinator: ThreadListCoordinatorProtocol { + + // MARK: - Properties + + // MARK: Private + + private let parameters: ThreadListCoordinatorParameters + private var threadListViewModel: ThreadListViewModelProtocol + private let threadListViewController: ThreadListViewController + + // MARK: Public + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + + weak var delegate: ThreadListCoordinatorDelegate? + + // MARK: - Setup + + init(parameters: ThreadListCoordinatorParameters) { + self.parameters = parameters + let threadListViewModel = ThreadListViewModel(session: self.parameters.session) + let threadListViewController = ThreadListViewController.instantiate(with: threadListViewModel) + self.threadListViewModel = threadListViewModel + self.threadListViewController = threadListViewController + } + + // MARK: - Public + + func start() { + self.threadListViewModel.coordinatorDelegate = self + } + + func toPresentable() -> UIViewController { + return self.threadListViewController + } +} + +// MARK: - ThreadListViewModelCoordinatorDelegate +extension ThreadListCoordinator: ThreadListViewModelCoordinatorDelegate { + + func threadListViewModel(_ viewModel: ThreadListViewModelProtocol, didCompleteWithUserDisplayName userDisplayName: String?) { + self.delegate?.threadListCoordinator(self, didCompleteWithUserDisplayName: userDisplayName) + } + + func threadListViewModelDidCancel(_ viewModel: ThreadListViewModelProtocol) { + self.delegate?.threadListCoordinatorDidCancel(self) + } +} diff --git a/Riot/Modules/Threads/ThreadList/ThreadListCoordinatorParameters.swift b/Riot/Modules/Threads/ThreadList/ThreadListCoordinatorParameters.swift new file mode 100644 index 000000000..9de96dcf3 --- /dev/null +++ b/Riot/Modules/Threads/ThreadList/ThreadListCoordinatorParameters.swift @@ -0,0 +1,28 @@ +// File created from ScreenTemplate +// $ createScreen.sh Threads/ThreadList ThreadList +/* + 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 Foundation + +/// ThreadListCoordinator input parameters +struct ThreadListCoordinatorParameters { + + /// The Matrix session + let session: MXSession + + +} diff --git a/Riot/Modules/Threads/ThreadList/ThreadListCoordinatorProtocol.swift b/Riot/Modules/Threads/ThreadList/ThreadListCoordinatorProtocol.swift new file mode 100644 index 000000000..b7f6c99f2 --- /dev/null +++ b/Riot/Modules/Threads/ThreadList/ThreadListCoordinatorProtocol.swift @@ -0,0 +1,29 @@ +// File created from ScreenTemplate +// $ createScreen.sh Threads/ThreadList ThreadList +/* + 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 Foundation + +protocol ThreadListCoordinatorDelegate: AnyObject { + func threadListCoordinator(_ coordinator: ThreadListCoordinatorProtocol, didCompleteWithUserDisplayName userDisplayName: String?) + func threadListCoordinatorDidCancel(_ coordinator: ThreadListCoordinatorProtocol) +} + +/// `ThreadListCoordinatorProtocol` is a protocol describing a Coordinator that handle xxxxxxx navigation flow. +protocol ThreadListCoordinatorProtocol: Coordinator, Presentable { + var delegate: ThreadListCoordinatorDelegate? { get } +} diff --git a/Riot/Modules/Threads/ThreadList/ThreadListViewAction.swift b/Riot/Modules/Threads/ThreadList/ThreadListViewAction.swift new file mode 100644 index 000000000..f602660fa --- /dev/null +++ b/Riot/Modules/Threads/ThreadList/ThreadListViewAction.swift @@ -0,0 +1,26 @@ +// File created from ScreenTemplate +// $ createScreen.sh Threads/ThreadList ThreadList +/* + 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 Foundation + +/// ThreadListViewController view actions exposed to view model +enum ThreadListViewAction { + case loadData + case complete + case cancel +} diff --git a/Riot/Modules/Threads/ThreadList/ThreadListViewController.storyboard b/Riot/Modules/Threads/ThreadList/ThreadListViewController.storyboard new file mode 100644 index 000000000..e5e0b2206 --- /dev/null +++ b/Riot/Modules/Threads/ThreadList/ThreadListViewController.storyboard @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Threads/ThreadList/ThreadListViewController.swift b/Riot/Modules/Threads/ThreadList/ThreadListViewController.swift new file mode 100644 index 000000000..d191381bf --- /dev/null +++ b/Riot/Modules/Threads/ThreadList/ThreadListViewController.swift @@ -0,0 +1,180 @@ +// File created from ScreenTemplate +// $ createScreen.sh Threads/ThreadList ThreadList +/* + 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 UIKit + +final class ThreadListViewController: UIViewController { + + // MARK: - Constants + + private enum Constants { + static let aConstant: Int = 666 + } + + // MARK: - Properties + + // MARK: Outlets + + @IBOutlet private weak var scrollView: UIScrollView! + + @IBOutlet private weak var informationLabel: UILabel! + @IBOutlet private weak var doneButton: UIButton! + + // MARK: Private + + private var viewModel: ThreadListViewModelProtocol! + private var theme: Theme! + private var keyboardAvoider: KeyboardAvoider? + private var errorPresenter: MXKErrorPresentation! + private var activityPresenter: ActivityIndicatorPresenter! + + // MARK: - Setup + + class func instantiate(with viewModel: ThreadListViewModelProtocol) -> ThreadListViewController { + let viewController = StoryboardScene.ThreadListViewController.initialScene.instantiate() + viewController.viewModel = viewModel + viewController.theme = ThemeService.shared().theme + return viewController + } + + // MARK: - Life cycle + + override func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + + self.setupViews() + self.keyboardAvoider = KeyboardAvoider(scrollViewContainerView: self.view, scrollView: self.scrollView) + self.activityPresenter = ActivityIndicatorPresenter() + self.errorPresenter = MXKErrorAlertPresentation() + + self.registerThemeServiceDidChangeThemeNotification() + self.update(theme: self.theme) + + self.viewModel.viewDelegate = self + + self.viewModel.process(viewAction: .loadData) + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + self.keyboardAvoider?.startAvoiding() + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + self.keyboardAvoider?.stopAvoiding() + } + + override var preferredStatusBarStyle: UIStatusBarStyle { + return self.theme.statusBarStyle + } + + // MARK: - Private + + private func update(theme: Theme) { + self.theme = theme + + self.view.backgroundColor = theme.headerBackgroundColor + + if let navigationBar = self.navigationController?.navigationBar { + theme.applyStyle(onNavigationBar: navigationBar) + } + + + // TODO: Set view colors here + self.informationLabel.textColor = theme.textPrimaryColor + + self.doneButton.backgroundColor = theme.backgroundColor + theme.applyStyle(onButton: self.doneButton) + } + + private func registerThemeServiceDidChangeThemeNotification() { + NotificationCenter.default.addObserver(self, selector: #selector(themeDidChange), name: .themeServiceDidChangeTheme, object: nil) + } + + @objc private func themeDidChange() { + self.update(theme: ThemeService.shared().theme) + } + + private func setupViews() { + let cancelBarButtonItem = MXKBarButtonItem(title: VectorL10n.cancel, style: .plain) { [weak self] in + self?.cancelButtonAction() + } + + self.navigationItem.rightBarButtonItem = cancelBarButtonItem + + self.title = "Template" + + self.scrollView.keyboardDismissMode = .interactive + + self.informationLabel.text = "VectorL10n.threadListTitle" + } + + private func render(viewState: ThreadListViewState) { + switch viewState { + case .idle: + break + case .loading: + self.renderLoading() + case .loaded(let displayName): + self.renderLoaded(displayName: displayName) + case .error(let error): + self.render(error: error) + } + } + + private func renderLoading() { + self.activityPresenter.presentActivityIndicator(on: self.view, animated: true) + self.informationLabel.text = "Fetch display name" + } + + private func renderLoaded(displayName: String) { + self.activityPresenter.removeCurrentActivityIndicator(animated: true) + + self.informationLabel.text = "You display name: \(displayName)" + } + + private func render(error: Error) { + self.activityPresenter.removeCurrentActivityIndicator(animated: true) + self.errorPresenter.presentError(from: self, forError: error, animated: true, handler: nil) + } + + + // MARK: - Actions + + @IBAction private func doneButtonAction(_ sender: Any) { + self.viewModel.process(viewAction: .complete) + } + + private func cancelButtonAction() { + self.viewModel.process(viewAction: .cancel) + } +} + + +// MARK: - ThreadListViewModelViewDelegate +extension ThreadListViewController: ThreadListViewModelViewDelegate { + + func threadListViewModel(_ viewModel: ThreadListViewModelProtocol, didUpdateViewState viewSate: ThreadListViewState) { + self.render(viewState: viewSate) + } +} diff --git a/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift b/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift new file mode 100644 index 000000000..08688ab4d --- /dev/null +++ b/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift @@ -0,0 +1,93 @@ +// File created from ScreenTemplate +// $ createScreen.sh Threads/ThreadList ThreadList +/* + 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 Foundation + +final class ThreadListViewModel: ThreadListViewModelProtocol { + + // MARK: - Properties + + // MARK: Private + + private let session: MXSession + + private var currentOperation: MXHTTPOperation? + private var userDisplayName: String? + + // MARK: Public + + weak var viewDelegate: ThreadListViewModelViewDelegate? + weak var coordinatorDelegate: ThreadListViewModelCoordinatorDelegate? + + private(set) var viewState: ThreadListViewState = .idle { + didSet { + self.viewDelegate?.threadListViewModel(self, didUpdateViewState: viewState) + } + } + + // MARK: - Setup + + init(session: MXSession) { + self.session = session + } + + deinit { + self.cancelOperations() + } + + // MARK: - Public + + func process(viewAction: ThreadListViewAction) { + switch viewAction { + case .loadData: + self.loadData() + case .complete: + self.coordinatorDelegate?.threadListViewModel(self, didCompleteWithUserDisplayName: self.userDisplayName) + case .cancel: + self.cancelOperations() + self.coordinatorDelegate?.threadListViewModelDidCancel(self) + } + } + + // MARK: - Private + + private func loadData() { + + viewState = .loading + + // Check first that the user homeserver is federated with the Riot-bot homeserver + self.currentOperation = self.session.matrixRestClient.displayName(forUser: self.session.myUser.userId) { [weak self] (response) in + + guard let self = self else { + return + } + + switch response { + case .success(let userDisplayName): + self.viewState = .loaded(userDisplayName) + self.userDisplayName = userDisplayName + case .failure(let error): + self.viewState = .error(error) + } + } + } + + private func cancelOperations() { + self.currentOperation?.cancel() + } +} diff --git a/Riot/Modules/Threads/ThreadList/ThreadListViewModelProtocol.swift b/Riot/Modules/Threads/ThreadList/ThreadListViewModelProtocol.swift new file mode 100644 index 000000000..8e0716d0c --- /dev/null +++ b/Riot/Modules/Threads/ThreadList/ThreadListViewModelProtocol.swift @@ -0,0 +1,39 @@ +// File created from ScreenTemplate +// $ createScreen.sh Threads/ThreadList ThreadList +/* + 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 Foundation + +protocol ThreadListViewModelViewDelegate: AnyObject { + func threadListViewModel(_ viewModel: ThreadListViewModelProtocol, didUpdateViewState viewSate: ThreadListViewState) +} + +protocol ThreadListViewModelCoordinatorDelegate: AnyObject { + func threadListViewModel(_ viewModel: ThreadListViewModelProtocol, didCompleteWithUserDisplayName userDisplayName: String?) + func threadListViewModelDidCancel(_ viewModel: ThreadListViewModelProtocol) +} + +/// Protocol describing the view model used by `ThreadListViewController` +protocol ThreadListViewModelProtocol { + + var viewDelegate: ThreadListViewModelViewDelegate? { get set } + var coordinatorDelegate: ThreadListViewModelCoordinatorDelegate? { get set } + + func process(viewAction: ThreadListViewAction) + + var viewState: ThreadListViewState { get } +} diff --git a/Riot/Modules/Threads/ThreadList/ThreadListViewState.swift b/Riot/Modules/Threads/ThreadList/ThreadListViewState.swift new file mode 100644 index 000000000..2d5f11e5d --- /dev/null +++ b/Riot/Modules/Threads/ThreadList/ThreadListViewState.swift @@ -0,0 +1,27 @@ +// File created from ScreenTemplate +// $ createScreen.sh Threads/ThreadList ThreadList +/* + 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 Foundation + +/// ThreadListViewController view state +enum ThreadListViewState { + case idle + case loading + case loaded(_ displayName: String) + case error(Error) +} diff --git a/Riot/Modules/Threads/ThreadsCoordinator.swift b/Riot/Modules/Threads/ThreadsCoordinator.swift new file mode 100644 index 000000000..a51e76e5f --- /dev/null +++ b/Riot/Modules/Threads/ThreadsCoordinator.swift @@ -0,0 +1,102 @@ +// File created from FlowTemplate +// $ createRootCoordinator.sh Threads Threads ThreadList +/* + 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 UIKit + +@objcMembers +final class ThreadsCoordinator: NSObject, ThreadsCoordinatorProtocol { + + // MARK: - Properties + + // MARK: Private + + private let parameters: ThreadsCoordinatorParameters + + private var navigationRouter: NavigationRouterType { + return self.parameters.navigationRouter + } + + // MARK: Public + + // Must be used only internally + var childCoordinators: [Coordinator] = [] + + weak var delegate: ThreadsCoordinatorDelegate? + + // MARK: - Setup + + init(parameters: ThreadsCoordinatorParameters) { + self.parameters = parameters + } + + // MARK: - Public + + func start() { + + let rootCoordinator = self.createThreadListCoordinator() + + rootCoordinator.start() + + self.add(childCoordinator: rootCoordinator) + + // Detect when view controller has been dismissed by gesture when presented modally (not in full screen). + self.navigationRouter.toPresentable().presentationController?.delegate = self + + if self.navigationRouter.modules.isEmpty == false { + self.navigationRouter.push(rootCoordinator, animated: true, popCompletion: { [weak self] in + self?.remove(childCoordinator: rootCoordinator) + }) + } else { + self.navigationRouter.setRootModule(rootCoordinator) { [weak self] in + self?.remove(childCoordinator: rootCoordinator) + } + } + } + + func toPresentable() -> UIViewController { + return self.navigationRouter.toPresentable() + } + + // MARK: - Private + + private func createThreadListCoordinator() -> ThreadListCoordinator { + let coordinatorParameters = ThreadListCoordinatorParameters(session: self.parameters.session) + let coordinator = ThreadListCoordinator(parameters: coordinatorParameters) + coordinator.delegate = self + return coordinator + } +} + +// MARK: - UIAdaptivePresentationControllerDelegate +extension ThreadsCoordinator: UIAdaptivePresentationControllerDelegate { + + func presentationControllerDidDismiss(_ presentationController: UIPresentationController) { + self.delegate?.threadsCoordinatorDidDismissInteractively(self) + } +} + +// MARK: - ThreadListCoordinatorDelegate +extension ThreadsCoordinator: ThreadListCoordinatorDelegate { + func threadListCoordinator(_ coordinator: ThreadListCoordinatorProtocol, didCompleteWithUserDisplayName userDisplayName: String?) { + self.delegate?.threadsCoordinatorDidComplete(self) + } + + func threadListCoordinatorDidCancel(_ coordinator: ThreadListCoordinatorProtocol) { + self.delegate?.threadsCoordinatorDidComplete(self) + } +} diff --git a/Riot/Modules/Threads/ThreadsCoordinatorBridgePresenter.swift b/Riot/Modules/Threads/ThreadsCoordinatorBridgePresenter.swift new file mode 100644 index 000000000..6ec252055 --- /dev/null +++ b/Riot/Modules/Threads/ThreadsCoordinatorBridgePresenter.swift @@ -0,0 +1,139 @@ +// File created from FlowTemplate +// $ createRootCoordinator.sh Threads Threads ThreadList +/* + 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 Foundation + +@objc protocol ThreadsCoordinatorBridgePresenterDelegate { + func threadsCoordinatorBridgePresenterDelegateDidComplete(_ coordinatorBridgePresenter: ThreadsCoordinatorBridgePresenter) + func threadsCoordinatorBridgePresenterDidDismissInteractively(_ coordinatorBridgePresenter: ThreadsCoordinatorBridgePresenter) +} + +/// ThreadsCoordinatorBridgePresenter enables to start ThreadsCoordinator from a view controller. +/// This bridge is used while waiting for global usage of coordinator pattern. +/// **WARNING**: This class breaks the Coordinator abstraction and it has been introduced for **Objective-C compatibility only** (mainly for integration in legacy view controllers). Each bridge should be removed once the underlying Coordinator has been integrated by another Coordinator. +@objcMembers +final class ThreadsCoordinatorBridgePresenter: NSObject { + + // MARK: - Constants + + private enum NavigationType { + case present + case push + } + + // MARK: - Properties + + // MARK: Private + + private let session: MXSession + private let roomId: String + private var navigationType: NavigationType = .present + private var coordinator: ThreadsCoordinator? + + // MARK: Public + + weak var delegate: ThreadsCoordinatorBridgePresenterDelegate? + + // MARK: - Setup + + init(session: MXSession, + roomId: String) { + self.session = session + self.roomId = roomId + super.init() + } + + // MARK: - Public + + // NOTE: Default value feature is not compatible with Objective-C. + // func present(from viewController: UIViewController, animated: Bool) { + // self.present(from: viewController, animated: animated) + // } + + func present(from viewController: UIViewController, animated: Bool) { + + let threadsCoordinatorParameters = ThreadsCoordinatorParameters(session: self.session, + roomId: self.roomId) + + let threadsCoordinator = ThreadsCoordinator(parameters: threadsCoordinatorParameters) + threadsCoordinator.delegate = self + let presentable = threadsCoordinator.toPresentable() + viewController.present(presentable, animated: animated, completion: nil) + threadsCoordinator.start() + + self.coordinator = threadsCoordinator + self.navigationType = .present + } + + func push(from navigationController: UINavigationController, animated: Bool) { + + let navigationRouter = NavigationRouterStore.shared.navigationRouter(for: navigationController) + + let threadsCoordinatorParameters = ThreadsCoordinatorParameters(session: self.session, + roomId: self.roomId, + navigationRouter: navigationRouter) + + let threadsCoordinator = ThreadsCoordinator(parameters: threadsCoordinatorParameters) + threadsCoordinator.delegate = self + threadsCoordinator.start() // Will trigger the view controller push + + self.coordinator = threadsCoordinator + self.navigationType = .push + } + + func dismiss(animated: Bool, completion: (() -> Void)?) { + guard let coordinator = self.coordinator else { + return + } + + switch navigationType { + case .present: + // Dismiss modal + coordinator.toPresentable().dismiss(animated: animated) { + self.coordinator = nil + + if let completion = completion { + completion() + } + } + case .push: + // Pop view controller from UINavigationController + guard let navigationController = coordinator.toPresentable() as? UINavigationController else { + return + } + navigationController.popViewController(animated: animated) + self.coordinator = nil + + if let completion = completion { + completion() + } + } + } +} + +// MARK: - ThreadsCoordinatorDelegate +extension ThreadsCoordinatorBridgePresenter: ThreadsCoordinatorDelegate { + + func threadsCoordinatorDidComplete(_ coordinator: ThreadsCoordinatorProtocol) { + self.delegate?.threadsCoordinatorBridgePresenterDelegateDidComplete(self) + } + + func threadsCoordinatorDidDismissInteractively(_ coordinator: ThreadsCoordinatorProtocol) { + self.delegate?.threadsCoordinatorBridgePresenterDidDismissInteractively(self) + } +} diff --git a/Riot/Modules/Threads/ThreadsCoordinatorParameters.swift b/Riot/Modules/Threads/ThreadsCoordinatorParameters.swift new file mode 100644 index 000000000..3e21ad86e --- /dev/null +++ b/Riot/Modules/Threads/ThreadsCoordinatorParameters.swift @@ -0,0 +1,40 @@ +// File created from FlowTemplate +// $ createRootCoordinator.sh Threads Threads ThreadList +/* + 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 Foundation + +/// ThreadsCoordinator input parameters +struct ThreadsCoordinatorParameters { + + /// The Matrix session + let session: MXSession + + /// Room identifier + let roomId: String + + /// The navigation router that manage physical navigation + let navigationRouter: NavigationRouterType + + init(session: MXSession, + roomId: String, + navigationRouter: NavigationRouterType? = nil) { + self.session = session + self.roomId = roomId + self.navigationRouter = navigationRouter ?? NavigationRouter(navigationController: RiotNavigationController()) + } +} diff --git a/Riot/Modules/Threads/ThreadsCoordinatorProtocol.swift b/Riot/Modules/Threads/ThreadsCoordinatorProtocol.swift new file mode 100644 index 000000000..c342fa33d --- /dev/null +++ b/Riot/Modules/Threads/ThreadsCoordinatorProtocol.swift @@ -0,0 +1,31 @@ +// File created from FlowTemplate +// $ createRootCoordinator.sh Threads Threads ThreadList +/* + 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 Foundation + +protocol ThreadsCoordinatorDelegate: AnyObject { + func threadsCoordinatorDidComplete(_ coordinator: ThreadsCoordinatorProtocol) + + /// Called when the view has been dismissed by gesture when presented modally (not in full screen). + func threadsCoordinatorDidDismissInteractively(_ coordinator: ThreadsCoordinatorProtocol) +} + +/// `ThreadsCoordinatorProtocol` is a protocol describing a Coordinator that handle xxxxxxx navigation flow. +protocol ThreadsCoordinatorProtocol: Coordinator, Presentable { + var delegate: ThreadsCoordinatorDelegate? { get } +} From d62b4c0b886477bc31bc4e1173496d45caf297b5 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Thu, 18 Nov 2021 17:43:13 +0300 Subject: [PATCH 36/87] Update thread icons --- .../Thread.png | Bin 401 -> 318 bytes .../Thread@2x.png | Bin 637 -> 418 bytes .../Thread@3x.png | Bin 945 -> 706 bytes 3 files changed, 0 insertions(+), 0 deletions(-) diff --git a/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_reply_in_thread.imageset/Thread.png b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_reply_in_thread.imageset/Thread.png index 28f8fa589f5243d365bdcd815a9f1b2199bf712e..ec23ad509d45acc94beb51fef16bf7fdda0b0902 100644 GIT binary patch delta 276 zcmV+v0qg#e1HJ+wiBL{Q4GJ0x0000DNk~Le0000I0000I2nGNE09MY9SCJt!e*n`- zL_t(|0nL+54goCXHw ze5+3NtJm)>nqcr2yLN+VWR5Kqg>Vd3lM{`X+W~Iuv_RT^D_=E7m0xmIb5+SqfYP?^BYQ_G|8DXt|p$3 a3OoV1xkWwVAUh)f0000mzre*q>* zL_t(|0o9dFQo=A4fM41;o(q5xQUp zX?Pi$!a&22@H6z=L|L^BlDrjlS#kD-Re>-j@>@fukeD>zK{AB0=Y2lSgouiDT7>jN4rPy-B z4(yq|KR|Vc;OTvcgY#e`L>h0f=d10Yr>V%4W88s$-c=ayz+^X)`tP7# z58Si9SBRO?QJv$2^G_5nzyOSy2?D;AZl!~u6SdHBV48fE7c0-jbNq7WJ}Vuq@JfpF zgVPa97|UP_XjK&sRuR3n6W9TCo>-*v%-zXlFSP%34$c%*?_qKD!~(eh0000e+?Xix-eQsClsP}LKkt%WZ@jXu?I}(gXl7}E<#8a z)5mm-8c1$4grtr!G8!e53@wVRkYb}+o^F@LMpA=Ne3!*^QHay+GQ-FUsk7~}PAEht zv^BfISIh#wedLn|3&YlR%-Vg8if!0!%=e1el;HP!K|Z0U^X+8#bVm&<*Ga9f1dUBKBcd zRBDs7aiX@igujc-(i(_z4){W*TRu zVOGG9wTPw!1bq>H#R=!1uts8j9|h?KS0DM_M8*JjvU<50P{wafKa22(uiJP{!p}w` z46-BM%<3^P=m;#4@OJ6Il{(-$-T~Wq2W%5o)Q6+X=RK7h67{zMONB;&f5;crJw;TX z0U{1dH(rC$-@sI&vI7LT0^=urQ{{wNBL-xf^mH#7RlO+V=^83hIR?77CvWQK4IIHQ zqqgyRpqzT^yD4kP16XT)H)Sn(pw9Yk%F6RV-SyoRp9gH?9k7jez&1`hpbweLbewfS zAMd9^Ud;%JhFQJJc_2Q2e`*E{b;tS$a%HU1tE6 zsj(k`S3M#w7_Tu7VqF$9HJ#6Qot14)>R{x+B#64V_AWc-k-uUZEN?(&1h0#N$TkHt kpm$t4QDMWyPsZaKzocT*a}P#j-T(jq07*qoM6N<$f|HU7c>n+a diff --git a/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_reply_in_thread.imageset/Thread@3x.png b/Riot/Assets/Images.xcassets/Room/ContextMenu/room_context_menu_reply_in_thread.imageset/Thread@3x.png index b7400c74bbee2b09a697274eccb04201792a2da1..64c675d3cb529c1c471835cbe346b8bac7ca12cf 100644 GIT binary patch delta 667 zcmV;M0%ZNM2f_s*iBL{Q4GJ0x0000DNk~Le0000s0000s2nGNE0E|Uy-H{sdJRyK!Tc1&PK_>V7(YGyPAL(EH&C~ZoV=F{OYL4?%sV_oz33u0fF z2Q$An$k6o1vL%Yf7WnKY1E+W7_KRe8gw1QHKiM4uF(l|jZTI zc5yEmkCqm%_In=lSIf`pyE#>>>Kol(#1&QnMOb8)GgQS&m`Z&naTz^H_-O4*d|PZv z@vBPx-8PHT7-g)9=1G9zMFQ_SEkGiRsdNobVSJ_(05$`uYG*_zeME^8BEq95Mg^002ovPDHLkV1hc) BF6;mR delta 908 zcmV;719SYs1+fPqiBL{Q4GJ0x0000DNk~Le0000;0000;2nGNE09Ea?N0A{ke*;EI zL_t(|0qvW;ZWBQe$7k;@6^-)-F6l^G3go1q&KnSTft+m&Lg08I2~y-UzB~beCm=Ni zmMAFk8{m=-H1Z;vmOA3VHk$76|u}@|7E-4 zI`9anciL1`(f74G3uUll&bJ;DAqv$=ptw14A1JCvL&SiB#%_IeC z$nH)YMPKl335vzgiQ>KX+h_f>NvQ(OEay8EPn=tZp6l+WS(1~sdK`_5XgPmVnuQK6 zT7l}Xf&(D_4zwN^tJl%iC=8_>f(7_emYnF$p#afMe;QZQof&#b zV_JcPBkn1c@O`YR!C4~&i|GaDNug3*_L1kb1oLEL&ymZh$Aor#~=b@0$~~C z0`h`_4`2vm0(H(B5-;omOGs>bN{N`c?^QkQ4E*JYBfB94vI2R+}7(~DV z5wJi6ED!+;M8E}vH&&K6rs6p9ekflI(&nus(2Yw?ddWl?>;t_Dda-x? zgc4_hw&Sqg43uVt59|LYnU0j;Ck7a;dwBBlF*t4XU_q9*AO&IiFrI5+ecGmevNVit z^Ju#R40?Ov32nNzVD3Q?3-+-)+Fiu2Z6dP;O Date: Thu, 18 Nov 2021 17:46:14 +0300 Subject: [PATCH 37/87] Refactor summary view to be configurable via a view model --- .../Modules/Room/DataSources/RoomDataSource.m | 2 +- .../Views/Threads/ThreadSummaryView.swift | 62 ++++++++++++------- .../Room/Views/Threads/ThreadSummaryView.xib | 25 ++++---- .../Threads/ThreadSummaryViewModel.swift | 23 +++++++ 4 files changed, 74 insertions(+), 38 deletions(-) create mode 100644 Riot/Modules/Room/Views/Threads/ThreadSummaryViewModel.swift diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m index 3a06fee1c..70a4214c7 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.m +++ b/Riot/Modules/Room/DataSources/RoomDataSource.m @@ -514,7 +514,7 @@ const CGFloat kTypingCellHeight = 24; // display thread summary view if the component has a thread in the room timeline if (RiotSettings.shared.enableThreads && component.thread && !self.threadId) { - threadSummaryView = [ThreadSummaryView instantiateWithThread:component.thread]; + threadSummaryView = [[ThreadSummaryView alloc] initWithThread:component.thread]; threadSummaryView.delegate = self; [temporaryViews addObject:threadSummaryView]; diff --git a/Riot/Modules/Room/Views/Threads/ThreadSummaryView.swift b/Riot/Modules/Room/Views/Threads/ThreadSummaryView.swift index ff5d65c48..7aa13f872 100644 --- a/Riot/Modules/Room/Views/Threads/ThreadSummaryView.swift +++ b/Riot/Modules/Room/Views/Threads/ThreadSummaryView.swift @@ -28,6 +28,7 @@ class ThreadSummaryView: UIView { private enum Constants { static let viewHeight: CGFloat = 32 + static let viewDefaultWidth: CGFloat = 320 static let cornerRadius: CGFloat = 4 } @@ -36,11 +37,7 @@ class ThreadSummaryView: UIView { @IBOutlet private weak var lastMessageAvatarView: UserAvatarView! @IBOutlet private weak var lastMessageContentLabel: UILabel! - private(set) var thread: MXThread! { - didSet { - configure() - } - } + private(set) var thread: MXThread! private lazy var tapGestureRecognizer: UITapGestureRecognizer = { return UITapGestureRecognizer(target: self, action: #selector(tapped(_:))) @@ -50,31 +47,49 @@ class ThreadSummaryView: UIView { // MARK: - Setup - static func instantiate(withThread thread: MXThread) -> ThreadSummaryView { - let view = ThreadSummaryView.loadFromNib() - view.thread = thread - view.update(theme: ThemeService.shared().theme) - view.translatesAutoresizingMaskIntoConstraints = false - return view + init(withThread thread: MXThread) { + self.thread = thread + super.init(frame: CGRect(origin: .zero, + size: CGSize(width: Constants.viewDefaultWidth, + height: Constants.viewHeight))) + loadNibContent() + update(theme: ThemeService.shared().theme) + configure() } static func contentViewHeight(forThread thread: MXThread, fitting maxWidth: CGFloat) -> CGFloat { return Constants.viewHeight } + required init?(coder: NSCoder) { + super.init(coder: coder) + loadNibContent() + } + + @nonobjc func configure(withViewModel viewModel: ThreadSummaryViewModel) { + numberOfRepliesLabel.text = String(viewModel.numberOfReplies) + if let avatar = viewModel.lastMessageSenderAvatar { + lastMessageAvatarView.fill(with: avatar) + } else { + lastMessageAvatarView.avatarImageView.image = nil + } + lastMessageContentLabel.text = viewModel.lastMessageText + } + private func configure() { clipsToBounds = true layer.cornerRadius = Constants.cornerRadius addGestureRecognizer(tapGestureRecognizer) - guard let thread = thread else { return } - numberOfRepliesLabel.text = String(thread.numberOfReplies) - guard let lastMessage = thread.lastMessage else { + guard let thread = thread, + let lastMessage = thread.lastMessage, + let session = thread.session, + let eventFormatter = session.roomSummaryUpdateDelegate as? MXKEventFormatter, + let room = session.room(withRoomId: lastMessage.roomId) else { lastMessageAvatarView.avatarImageView.image = nil lastMessageContentLabel.text = nil return } - guard let session = thread.session else { return } let lastMessageSender = session.user(withUserId: lastMessage.sender) let fallbackImage = AvatarFallbackImage.matrixItem(lastMessage.sender, @@ -84,17 +99,16 @@ class ThreadSummaryView: UIView { avatarUrl: lastMessageSender?.avatarUrl, mediaManager: session.mediaManager, fallbackImage: fallbackImage) - lastMessageAvatarView.fill(with: avatarViewData) - - guard let eventFormatter = session.roomSummaryUpdateDelegate as? MXKEventFormatter, - let room = session.room(withRoomId: lastMessage.roomId) else { - return - } room.state { [weak self] roomState in guard let self = self else { return } let formatterError = UnsafeMutablePointer.allocate(capacity: 1) - self.lastMessageContentLabel.text = eventFormatter.string(from: lastMessage, with: roomState, error: formatterError) + let lastMessageText = eventFormatter.string(from: lastMessage, with: roomState, error: formatterError) + + let viewModel = ThreadSummaryViewModel(numberOfReplies: thread.numberOfReplies, + lastMessageSenderAvatar: avatarViewData, + lastMessageText: lastMessageText) + self.configure(withViewModel: viewModel) } } @@ -107,7 +121,9 @@ class ThreadSummaryView: UIView { } } -extension ThreadSummaryView: NibLoadable {} +// extension ThreadSummaryView: NibLoadable {} + +extension ThreadSummaryView: NibOwnerLoadable {} extension ThreadSummaryView: Themable { diff --git a/Riot/Modules/Room/Views/Threads/ThreadSummaryView.xib b/Riot/Modules/Room/Views/Threads/ThreadSummaryView.xib index 15df38c54..62e9a2d6c 100644 --- a/Riot/Modules/Room/Views/Threads/ThreadSummaryView.xib +++ b/Riot/Modules/Room/Views/Threads/ThreadSummaryView.xib @@ -5,13 +5,19 @@ - - + + + + + + + + - + @@ -66,7 +72,7 @@ - + @@ -74,19 +80,10 @@ - - - - - - - - - - + diff --git a/Riot/Modules/Room/Views/Threads/ThreadSummaryViewModel.swift b/Riot/Modules/Room/Views/Threads/ThreadSummaryViewModel.swift new file mode 100644 index 000000000..11b68dda5 --- /dev/null +++ b/Riot/Modules/Room/Views/Threads/ThreadSummaryViewModel.swift @@ -0,0 +1,23 @@ +// +// 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 Foundation + +struct ThreadSummaryViewModel { + var numberOfReplies: Int + var lastMessageSenderAvatar: AvatarViewDataProtocol? + var lastMessageText: String? +} From 93f6abe1f423ea3392c70931aa1a32918660e75d Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Thu, 18 Nov 2021 17:49:01 +0300 Subject: [PATCH 38/87] Implement thread list --- .../ThreadList/ThreadListCoordinator.swift | 7 +- .../ThreadListCoordinatorParameters.swift | 3 +- .../ThreadListCoordinatorProtocol.swift | 2 +- .../ThreadListViewController.storyboard | 90 +++--------- .../ThreadList/ThreadListViewController.swift | 88 +++++++----- .../ThreadList/ThreadListViewModel.swift | 135 +++++++++++++++--- .../ThreadListViewModelProtocol.swift | 5 +- .../ThreadList/ThreadListViewState.swift | 2 +- .../Views/ThreadTableViewCell.swift | 67 +++++++++ .../ThreadList/Views/ThreadTableViewCell.xib | 85 +++++++++++ .../ThreadList/Views/ThreadViewModel.swift | 25 ++++ Riot/Modules/Threads/ThreadsCoordinator.swift | 5 +- 12 files changed, 388 insertions(+), 126 deletions(-) create mode 100644 Riot/Modules/Threads/ThreadList/Views/ThreadTableViewCell.swift create mode 100644 Riot/Modules/Threads/ThreadList/Views/ThreadTableViewCell.xib create mode 100644 Riot/Modules/Threads/ThreadList/Views/ThreadViewModel.swift diff --git a/Riot/Modules/Threads/ThreadList/ThreadListCoordinator.swift b/Riot/Modules/Threads/ThreadList/ThreadListCoordinator.swift index a672cf12a..00f647487 100644 --- a/Riot/Modules/Threads/ThreadList/ThreadListCoordinator.swift +++ b/Riot/Modules/Threads/ThreadList/ThreadListCoordinator.swift @@ -40,7 +40,8 @@ final class ThreadListCoordinator: ThreadListCoordinatorProtocol { init(parameters: ThreadListCoordinatorParameters) { self.parameters = parameters - let threadListViewModel = ThreadListViewModel(session: self.parameters.session) + let threadListViewModel = ThreadListViewModel(session: self.parameters.session, + roomId: self.parameters.roomId) let threadListViewController = ThreadListViewController.instantiate(with: threadListViewModel) self.threadListViewModel = threadListViewModel self.threadListViewController = threadListViewController @@ -60,8 +61,8 @@ final class ThreadListCoordinator: ThreadListCoordinatorProtocol { // MARK: - ThreadListViewModelCoordinatorDelegate extension ThreadListCoordinator: ThreadListViewModelCoordinatorDelegate { - func threadListViewModel(_ viewModel: ThreadListViewModelProtocol, didCompleteWithUserDisplayName userDisplayName: String?) { - self.delegate?.threadListCoordinator(self, didCompleteWithUserDisplayName: userDisplayName) + func threadListViewModelDidLoadThreads(_ viewModel: ThreadListViewModelProtocol) { + self.delegate?.threadListCoordinatorDidLoadThreads(self) } func threadListViewModelDidCancel(_ viewModel: ThreadListViewModelProtocol) { diff --git a/Riot/Modules/Threads/ThreadList/ThreadListCoordinatorParameters.swift b/Riot/Modules/Threads/ThreadList/ThreadListCoordinatorParameters.swift index 9de96dcf3..74c6b9885 100644 --- a/Riot/Modules/Threads/ThreadList/ThreadListCoordinatorParameters.swift +++ b/Riot/Modules/Threads/ThreadList/ThreadListCoordinatorParameters.swift @@ -24,5 +24,6 @@ struct ThreadListCoordinatorParameters { /// The Matrix session let session: MXSession - + /// Room identifier + let roomId: String } diff --git a/Riot/Modules/Threads/ThreadList/ThreadListCoordinatorProtocol.swift b/Riot/Modules/Threads/ThreadList/ThreadListCoordinatorProtocol.swift index b7f6c99f2..2680a53e1 100644 --- a/Riot/Modules/Threads/ThreadList/ThreadListCoordinatorProtocol.swift +++ b/Riot/Modules/Threads/ThreadList/ThreadListCoordinatorProtocol.swift @@ -19,7 +19,7 @@ import Foundation protocol ThreadListCoordinatorDelegate: AnyObject { - func threadListCoordinator(_ coordinator: ThreadListCoordinatorProtocol, didCompleteWithUserDisplayName userDisplayName: String?) + func threadListCoordinatorDidLoadThreads(_ coordinator: ThreadListCoordinatorProtocol) func threadListCoordinatorDidCancel(_ coordinator: ThreadListCoordinatorProtocol) } diff --git a/Riot/Modules/Threads/ThreadList/ThreadListViewController.storyboard b/Riot/Modules/Threads/ThreadList/ThreadListViewController.storyboard index e5e0b2206..b214de94a 100644 --- a/Riot/Modules/Threads/ThreadList/ThreadListViewController.storyboard +++ b/Riot/Modules/Threads/ThreadList/ThreadListViewController.storyboard @@ -1,16 +1,15 @@ - - - - + + - + + - + @@ -18,74 +17,26 @@ - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + - - - - + + + + - - - - + @@ -93,4 +44,9 @@ + + + + + diff --git a/Riot/Modules/Threads/ThreadList/ThreadListViewController.swift b/Riot/Modules/Threads/ThreadList/ThreadListViewController.swift index d191381bf..c45cf92f5 100644 --- a/Riot/Modules/Threads/ThreadList/ThreadListViewController.swift +++ b/Riot/Modules/Threads/ThreadList/ThreadListViewController.swift @@ -30,10 +30,7 @@ final class ThreadListViewController: UIViewController { // MARK: Outlets - @IBOutlet private weak var scrollView: UIScrollView! - - @IBOutlet private weak var informationLabel: UILabel! - @IBOutlet private weak var doneButton: UIButton! + @IBOutlet private weak var threadsTableView: UITableView! // MARK: Private @@ -60,7 +57,7 @@ final class ThreadListViewController: UIViewController { // Do any additional setup after loading the view. self.setupViews() - self.keyboardAvoider = KeyboardAvoider(scrollViewContainerView: self.view, scrollView: self.scrollView) + self.keyboardAvoider = KeyboardAvoider(scrollViewContainerView: self.view, scrollView: self.threadsTableView) self.activityPresenter = ActivityIndicatorPresenter() self.errorPresenter = MXKErrorAlertPresentation() @@ -99,12 +96,9 @@ final class ThreadListViewController: UIViewController { theme.applyStyle(onNavigationBar: navigationBar) } - - // TODO: Set view colors here - self.informationLabel.textColor = theme.textPrimaryColor - - self.doneButton.backgroundColor = theme.backgroundColor - theme.applyStyle(onButton: self.doneButton) + self.threadsTableView.backgroundColor = theme.backgroundColor + self.threadsTableView.separatorColor = theme.colors.separator + self.threadsTableView.reloadData() } private func registerThemeServiceDidChangeThemeNotification() { @@ -116,17 +110,18 @@ final class ThreadListViewController: UIViewController { } private func setupViews() { - let cancelBarButtonItem = MXKBarButtonItem(title: VectorL10n.cancel, style: .plain) { [weak self] in - self?.cancelButtonAction() - } + let filterBarButtonItem = UIBarButtonItem(image: Asset.Images.roomContextMenuMore.image, + style: .plain, + target: self, + action: #selector(filterButtonTapped(_:))) - self.navigationItem.rightBarButtonItem = cancelBarButtonItem + self.navigationItem.rightBarButtonItem = filterBarButtonItem - self.title = "Template" + self.title = "Threads" - self.scrollView.keyboardDismissMode = .interactive - - self.informationLabel.text = "VectorL10n.threadListTitle" + self.threadsTableView.tableFooterView = UIView() + self.threadsTableView.register(cellType: ThreadTableViewCell.self) + self.threadsTableView.keyboardDismissMode = .interactive } private func render(viewState: ThreadListViewState) { @@ -135,8 +130,8 @@ final class ThreadListViewController: UIViewController { break case .loading: self.renderLoading() - case .loaded(let displayName): - self.renderLoaded(displayName: displayName) + case .loaded: + self.renderLoaded() case .error(let error): self.render(error: error) } @@ -144,13 +139,11 @@ final class ThreadListViewController: UIViewController { private func renderLoading() { self.activityPresenter.presentActivityIndicator(on: self.view, animated: true) - self.informationLabel.text = "Fetch display name" } - private func renderLoaded(displayName: String) { + private func renderLoaded() { self.activityPresenter.removeCurrentActivityIndicator(animated: true) - - self.informationLabel.text = "You display name: \(displayName)" + self.threadsTableView.reloadData() } private func render(error: Error) { @@ -158,23 +151,54 @@ final class ThreadListViewController: UIViewController { self.errorPresenter.presentError(from: self, forError: error, animated: true, handler: nil) } - // MARK: - Actions - @IBAction private func doneButtonAction(_ sender: Any) { + @objc + private func filterButtonTapped(_ sender: UIBarButtonItem) { self.viewModel.process(viewAction: .complete) } - private func cancelButtonAction() { - self.viewModel.process(viewAction: .cancel) - } } - // MARK: - ThreadListViewModelViewDelegate + extension ThreadListViewController: ThreadListViewModelViewDelegate { - func threadListViewModel(_ viewModel: ThreadListViewModelProtocol, didUpdateViewState viewSate: ThreadListViewState) { + func threadListViewModel(_ viewModel: ThreadListViewModelProtocol, + didUpdateViewState viewSate: ThreadListViewState) { self.render(viewState: viewSate) } } + +extension ThreadListViewController: UITableViewDataSource { + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return viewModel.numberOfThreads + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell: ThreadTableViewCell = tableView.dequeueReusableCell(for: indexPath) + + if let threadVM = viewModel.threadViewModel(at: indexPath.row) { + cell.configure(withViewModel: threadVM) + } + cell.update(theme: theme) + + return cell + } + +} + +extension ThreadListViewController: UITableViewDelegate { + + func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { + cell.backgroundColor = theme.backgroundColor + cell.selectedBackgroundView = UIView() + cell.selectedBackgroundView?.backgroundColor = theme.selectedBackgroundColor + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + tableView.deselectRow(at: indexPath, animated: true) + } + +} diff --git a/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift b/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift index 08688ab4d..1674d2762 100644 --- a/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift +++ b/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift @@ -25,9 +25,12 @@ final class ThreadListViewModel: ThreadListViewModelProtocol { // MARK: Private private let session: MXSession + private let roomId: String + private var threads: [MXThread] = [] + private var eventFormatter: MXKEventFormatter? + private var roomState: MXRoomState? private var currentOperation: MXHTTPOperation? - private var userDisplayName: String? // MARK: Public @@ -42,8 +45,10 @@ final class ThreadListViewModel: ThreadListViewModelProtocol { // MARK: - Setup - init(session: MXSession) { + init(session: MXSession, + roomId: String) { self.session = session + self.roomId = roomId } deinit { @@ -57,37 +62,131 @@ final class ThreadListViewModel: ThreadListViewModelProtocol { case .loadData: self.loadData() case .complete: - self.coordinatorDelegate?.threadListViewModel(self, didCompleteWithUserDisplayName: self.userDisplayName) + self.coordinatorDelegate?.threadListViewModelDidLoadThreads(self) case .cancel: self.cancelOperations() self.coordinatorDelegate?.threadListViewModelDidCancel(self) } } + var numberOfThreads: Int { + return threads.count + } + + func threadViewModel(at index: Int) -> ThreadViewModel? { + guard index < threads.count else { + return nil + } + return viewModel(forThread: threads[index]) + } + // MARK: - Private + private func viewModel(forThread thread: MXThread) -> ThreadViewModel { + let rootAvatarViewData: AvatarViewData? + let rootMessageSender: MXUser? + let lastAvatarViewData: AvatarViewData? + let lastMessageSender: MXUser? + let rootMessageText: String? + let lastMessageText: String? + let lastMessageTime: String? + + // root message + if let rootMessage = thread.rootMessage, let senderId = rootMessage.sender { + rootMessageSender = session.user(withUserId: rootMessage.sender) + + let fallbackImage = AvatarFallbackImage.matrixItem(senderId, + rootMessageSender?.displayname) + rootAvatarViewData = AvatarViewData(matrixItemId: senderId, + displayName: rootMessageSender?.displayname, + avatarUrl: rootMessageSender?.avatarUrl, + mediaManager: session.mediaManager, + fallbackImage: fallbackImage) + } else { + rootAvatarViewData = nil + rootMessageSender = nil + } + + // last message + if let lastMessage = thread.lastMessage, let senderId = lastMessage.sender { + lastMessageSender = session.user(withUserId: lastMessage.sender) + + let fallbackImage = AvatarFallbackImage.matrixItem(senderId, + lastMessageSender?.displayname) + lastAvatarViewData = AvatarViewData(matrixItemId: senderId, + displayName: lastMessageSender?.displayname, + avatarUrl: lastMessageSender?.avatarUrl, + mediaManager: session.mediaManager, + fallbackImage: fallbackImage) + } else { + lastAvatarViewData = nil + lastMessageSender = nil + } + + if let eventFormatter = eventFormatter { + let formatterError = UnsafeMutablePointer.allocate(capacity: 1) + rootMessageText = eventFormatter.string(from: thread.rootMessage, + with: roomState, + error: formatterError) + lastMessageText = eventFormatter.string(from: thread.lastMessage, + with: roomState, + error: formatterError) + lastMessageTime = eventFormatter.dateString(from: thread.lastMessage, withTime: true) + } else { + rootMessageText = nil + lastMessageText = nil + lastMessageTime = nil + } + + let summaryViewModel = ThreadSummaryViewModel(numberOfReplies: thread.numberOfReplies, + lastMessageSenderAvatar: lastAvatarViewData, + lastMessageText: lastMessageText) + + return ThreadViewModel(rootMessageSenderAvatar: rootAvatarViewData, + rootMessageSenderDisplayName: rootMessageSender?.displayname, + rootMessageText: rootMessageText, + lastMessageTime: lastMessageTime, + summaryViewModel: summaryViewModel) + } + private func loadData() { viewState = .loading - - // Check first that the user homeserver is federated with the Riot-bot homeserver - self.currentOperation = self.session.matrixRestClient.displayName(forUser: self.session.myUser.userId) { [weak self] (response) in - - guard let self = self else { - return - } + + threads = session.threadingService.threads(inRoom: roomId) + session.threadingService.addDelegate(self) + threadsLoaded() + } + + private func threadsLoaded() { + guard let eventFormatter = session.roomSummaryUpdateDelegate as? MXKEventFormatter, + let room = session.room(withRoomId: roomId) else { + // go into loaded state + self.viewState = .loaded - switch response { - case .success(let userDisplayName): - self.viewState = .loaded(userDisplayName) - self.userDisplayName = userDisplayName - case .failure(let error): - self.viewState = .error(error) - } + return + } + + room.state { [weak self] roomState in + guard let self = self else { return } + self.eventFormatter = eventFormatter + self.roomState = roomState + + // go into loaded state + self.viewState = .loaded } } - + private func cancelOperations() { self.currentOperation?.cancel() } } + +extension ThreadListViewModel: MXThreadingServiceDelegate { + + func threadingServiceDidUpdateThreads(_ service: MXThreadingService) { + threads = service.threads(inRoom: roomId) + viewState = .loaded + } + +} diff --git a/Riot/Modules/Threads/ThreadList/ThreadListViewModelProtocol.swift b/Riot/Modules/Threads/ThreadList/ThreadListViewModelProtocol.swift index 8e0716d0c..1b8ea9024 100644 --- a/Riot/Modules/Threads/ThreadList/ThreadListViewModelProtocol.swift +++ b/Riot/Modules/Threads/ThreadList/ThreadListViewModelProtocol.swift @@ -23,7 +23,7 @@ protocol ThreadListViewModelViewDelegate: AnyObject { } protocol ThreadListViewModelCoordinatorDelegate: AnyObject { - func threadListViewModel(_ viewModel: ThreadListViewModelProtocol, didCompleteWithUserDisplayName userDisplayName: String?) + func threadListViewModelDidLoadThreads(_ viewModel: ThreadListViewModelProtocol) func threadListViewModelDidCancel(_ viewModel: ThreadListViewModelProtocol) } @@ -36,4 +36,7 @@ protocol ThreadListViewModelProtocol { func process(viewAction: ThreadListViewAction) var viewState: ThreadListViewState { get } + + var numberOfThreads: Int { get } + func threadViewModel(at index: Int) -> ThreadViewModel? } diff --git a/Riot/Modules/Threads/ThreadList/ThreadListViewState.swift b/Riot/Modules/Threads/ThreadList/ThreadListViewState.swift index 2d5f11e5d..8d7a742d2 100644 --- a/Riot/Modules/Threads/ThreadList/ThreadListViewState.swift +++ b/Riot/Modules/Threads/ThreadList/ThreadListViewState.swift @@ -22,6 +22,6 @@ import Foundation enum ThreadListViewState { case idle case loading - case loaded(_ displayName: String) + case loaded case error(Error) } diff --git a/Riot/Modules/Threads/ThreadList/Views/ThreadTableViewCell.swift b/Riot/Modules/Threads/ThreadList/Views/ThreadTableViewCell.swift new file mode 100644 index 000000000..147b87f4b --- /dev/null +++ b/Riot/Modules/Threads/ThreadList/Views/ThreadTableViewCell.swift @@ -0,0 +1,67 @@ +// +// 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 UIKit +import Reusable + +class ThreadTableViewCell: UITableViewCell { + + private enum Constants { + static let separatorInset: UIEdgeInsets = UIEdgeInsets(top: 0, left: 56, bottom: 0, right: 0) + } + + @IBOutlet private weak var rootMessageAvatarView: UserAvatarView! + @IBOutlet private weak var rootMessageSenderLabel: UILabel! + @IBOutlet private weak var rootMessageContentLabel: UILabel! + @IBOutlet private weak var lastMessageTimeLabel: UILabel! + @IBOutlet private weak var summaryView: ThreadSummaryView! + + override func awakeFromNib() { + super.awakeFromNib() + + separatorInset = Constants.separatorInset + } + + func configure(withViewModel viewModel: ThreadViewModel) { + if let rootAvatar = viewModel.rootMessageSenderAvatar { + rootMessageAvatarView.fill(with: rootAvatar) + } else { + rootMessageAvatarView.avatarImageView.image = nil + } + rootMessageSenderLabel.text = viewModel.rootMessageSenderDisplayName + rootMessageContentLabel.text = viewModel.rootMessageText + lastMessageTimeLabel.text = viewModel.lastMessageTime + if let summaryViewModel = viewModel.summaryViewModel { + summaryView.configure(withViewModel: summaryViewModel) + } + } + +} + +extension ThreadTableViewCell: NibReusable {} + +extension ThreadTableViewCell: Themable { + + func update(theme: Theme) { + contentView.backgroundColor = theme.colors.background + rootMessageAvatarView.backgroundColor = .clear + rootMessageContentLabel.textColor = theme.colors.primaryContent + lastMessageTimeLabel.textColor = theme.colors.secondaryContent + summaryView.update(theme: theme) + summaryView.backgroundColor = .clear + } + +} diff --git a/Riot/Modules/Threads/ThreadList/Views/ThreadTableViewCell.xib b/Riot/Modules/Threads/ThreadList/Views/ThreadTableViewCell.xib new file mode 100644 index 000000000..faea34907 --- /dev/null +++ b/Riot/Modules/Threads/ThreadList/Views/ThreadTableViewCell.xib @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Threads/ThreadList/Views/ThreadViewModel.swift b/Riot/Modules/Threads/ThreadList/Views/ThreadViewModel.swift new file mode 100644 index 000000000..9bc842345 --- /dev/null +++ b/Riot/Modules/Threads/ThreadList/Views/ThreadViewModel.swift @@ -0,0 +1,25 @@ +// +// 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 Foundation + +struct ThreadViewModel { + var rootMessageSenderAvatar: AvatarViewDataProtocol? + var rootMessageSenderDisplayName: String? + var rootMessageText: String? + var lastMessageTime: String? + var summaryViewModel: ThreadSummaryViewModel? +} diff --git a/Riot/Modules/Threads/ThreadsCoordinator.swift b/Riot/Modules/Threads/ThreadsCoordinator.swift index a51e76e5f..e52c46156 100644 --- a/Riot/Modules/Threads/ThreadsCoordinator.swift +++ b/Riot/Modules/Threads/ThreadsCoordinator.swift @@ -75,7 +75,8 @@ final class ThreadsCoordinator: NSObject, ThreadsCoordinatorProtocol { // MARK: - Private private func createThreadListCoordinator() -> ThreadListCoordinator { - let coordinatorParameters = ThreadListCoordinatorParameters(session: self.parameters.session) + let coordinatorParameters = ThreadListCoordinatorParameters(session: self.parameters.session, + roomId: self.parameters.roomId) let coordinator = ThreadListCoordinator(parameters: coordinatorParameters) coordinator.delegate = self return coordinator @@ -92,7 +93,7 @@ extension ThreadsCoordinator: UIAdaptivePresentationControllerDelegate { // MARK: - ThreadListCoordinatorDelegate extension ThreadsCoordinator: ThreadListCoordinatorDelegate { - func threadListCoordinator(_ coordinator: ThreadListCoordinatorProtocol, didCompleteWithUserDisplayName userDisplayName: String?) { + func threadListCoordinatorDidLoadThreads(_ coordinator: ThreadListCoordinatorProtocol) { self.delegate?.threadsCoordinatorDidComplete(self) } From 827b81f1bc1d3a55355bd673c2537d6b2b85bbad Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Fri, 19 Nov 2021 02:12:38 +0300 Subject: [PATCH 39/87] Little refactorings --- .../ThreadList/ThreadListViewModel.swift | 49 ++++++++++++------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift b/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift index 1674d2762..f8fa49806 100644 --- a/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift +++ b/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift @@ -87,9 +87,8 @@ final class ThreadListViewModel: ThreadListViewModelProtocol { let rootMessageSender: MXUser? let lastAvatarViewData: AvatarViewData? let lastMessageSender: MXUser? - let rootMessageText: String? - let lastMessageText: String? - let lastMessageTime: String? + let rootMessageText = rootMessageText(forThread: thread) + let (lastMessageText, lastMessageTime) = lastMessageTextAndTime(forThread: thread) // root message if let rootMessage = thread.rootMessage, let senderId = rootMessage.sender { @@ -123,21 +122,6 @@ final class ThreadListViewModel: ThreadListViewModelProtocol { lastMessageSender = nil } - if let eventFormatter = eventFormatter { - let formatterError = UnsafeMutablePointer.allocate(capacity: 1) - rootMessageText = eventFormatter.string(from: thread.rootMessage, - with: roomState, - error: formatterError) - lastMessageText = eventFormatter.string(from: thread.lastMessage, - with: roomState, - error: formatterError) - lastMessageTime = eventFormatter.dateString(from: thread.lastMessage, withTime: true) - } else { - rootMessageText = nil - lastMessageText = nil - lastMessageTime = nil - } - let summaryViewModel = ThreadSummaryViewModel(numberOfReplies: thread.numberOfReplies, lastMessageSenderAvatar: lastAvatarViewData, lastMessageText: lastMessageText) @@ -149,6 +133,35 @@ final class ThreadListViewModel: ThreadListViewModelProtocol { summaryViewModel: summaryViewModel) } + private func rootMessageText(forThread thread: MXThread) -> String? { + guard let eventFormatter = eventFormatter else { + return nil + } + guard let message = thread.rootMessage else { + return nil + } + let formatterError = UnsafeMutablePointer.allocate(capacity: 1) + return eventFormatter.string(from: message, + with: roomState, + error: formatterError) + } + + private func lastMessageTextAndTime(forThread thread: MXThread) -> (String?, String?) { + guard let eventFormatter = eventFormatter else { + return (nil, nil) + } + guard let message = thread.lastMessage else { + return (nil, nil) + } + let formatterError = UnsafeMutablePointer.allocate(capacity: 1) + return ( + eventFormatter.string(from: message, + with: roomState, + error: formatterError), + eventFormatter.dateString(from: message, withTime: true) + ) + } + private func loadData() { viewState = .loading From 84f987afa33c4d9b29d94003c71f286383ed0b71 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Fri, 19 Nov 2021 02:17:01 +0300 Subject: [PATCH 40/87] Fix build --- Riot/Modules/Room/RoomCoordinator.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Modules/Room/RoomCoordinator.swift b/Riot/Modules/Room/RoomCoordinator.swift index 59e405aa6..51956f109 100644 --- a/Riot/Modules/Room/RoomCoordinator.swift +++ b/Riot/Modules/Room/RoomCoordinator.swift @@ -109,7 +109,7 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { } else if let threadId = self.parameters.threadId { self.loadRoom(withId: self.parameters.roomId, andThreadId: threadId, completion: completion) } else if let eventId = self.selectedEventId { - self.loadRoom(withId: self.parameters.roomId, and: eventId, completion: completion) + self.loadRoom(withId: self.parameters.roomId, andEventId: eventId, completion: completion) } else { self.loadRoom(withId: self.parameters.roomId, completion: completion) } From d34f5d43f3b5c759596c349d37abab22f88aaf66 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Fri, 19 Nov 2021 03:36:51 +0300 Subject: [PATCH 41/87] Add my threads filtering --- Riot/Assets/en.lproj/Vector.strings | 3 ++ Riot/Generated/Strings.swift | 12 ++++++ .../ThreadList/ThreadListViewAction.swift | 2 + .../ThreadList/ThreadListViewController.swift | 40 ++++++++++++++++++- .../ThreadList/ThreadListViewModel.swift | 24 ++++++++--- .../ThreadListViewModelProtocol.swift | 15 +++++++ .../ThreadList/ThreadListViewState.swift | 1 + 7 files changed, 89 insertions(+), 8 deletions(-) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index dabfba721..3514e9dfc 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -418,6 +418,9 @@ Tap the + to start adding people."; // MARK: Threads "room_thread_title" = "Thread"; +"threads_title" = "Threads"; +"threads_action_all_threads" = "All threads"; +"threads_action_my_threads" = "My threads"; "media_type_accessibility_image" = "Image"; "media_type_accessibility_audio" = "Audio"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index bb9ac8ac7..9fbf8ec5c 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -5067,6 +5067,18 @@ public class VectorL10n: NSObject { public static var `switch`: String { return VectorL10n.tr("Vector", "switch") } + /// All threads + public static var threadsActionAllThreads: String { + return VectorL10n.tr("Vector", "threads_action_all_threads") + } + /// My threads + public static var threadsActionMyThreads: String { + return VectorL10n.tr("Vector", "threads_action_my_threads") + } + /// Threads + public static var threadsTitle: String { + return VectorL10n.tr("Vector", "threads_title") + } /// Favourites public static var titleFavourites: String { return VectorL10n.tr("Vector", "title_favourites") diff --git a/Riot/Modules/Threads/ThreadList/ThreadListViewAction.swift b/Riot/Modules/Threads/ThreadList/ThreadListViewAction.swift index f602660fa..fe2f0ceb9 100644 --- a/Riot/Modules/Threads/ThreadList/ThreadListViewAction.swift +++ b/Riot/Modules/Threads/ThreadList/ThreadListViewAction.swift @@ -21,6 +21,8 @@ import Foundation /// ThreadListViewController view actions exposed to view model enum ThreadListViewAction { case loadData + case showFilterTypes + case selectFilterType(_ type: ThreadListFilterType) case complete case cancel } diff --git a/Riot/Modules/Threads/ThreadList/ThreadListViewController.swift b/Riot/Modules/Threads/ThreadList/ThreadListViewController.swift index c45cf92f5..6b8abfffe 100644 --- a/Riot/Modules/Threads/ThreadList/ThreadListViewController.swift +++ b/Riot/Modules/Threads/ThreadList/ThreadListViewController.swift @@ -117,7 +117,7 @@ final class ThreadListViewController: UIViewController { self.navigationItem.rightBarButtonItem = filterBarButtonItem - self.title = "Threads" + self.title = VectorL10n.threadsTitle self.threadsTableView.tableFooterView = UIView() self.threadsTableView.register(cellType: ThreadTableViewCell.self) @@ -132,6 +132,8 @@ final class ThreadListViewController: UIViewController { self.renderLoading() case .loaded: self.renderLoaded() + case .showingFilterTypes: + self.renderShowingFilterTypes() case .error(let error): self.render(error: error) } @@ -146,6 +148,40 @@ final class ThreadListViewController: UIViewController { self.threadsTableView.reloadData() } + private func renderShowingFilterTypes() { + let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + + let allThreadsAction = UIAlertAction(title: ThreadListFilterType.all.title, + style: .default, + handler: { [weak self] action in + guard let self = self else { return } + self.viewModel.process(viewAction: .selectFilterType(.all)) + }) + if self.viewModel.selectedFilterType == .all { + allThreadsAction.setValue(true, forKey: "checked") + } + alertController.addAction(allThreadsAction) + + let myThreadsAction = UIAlertAction(title: ThreadListFilterType.myThreads.title, + style: .default, + handler: { [weak self] action in + guard let self = self else { return } + self.viewModel.process(viewAction: .selectFilterType(.myThreads)) + }) + if self.viewModel.selectedFilterType == .myThreads { + myThreadsAction.setValue(true, forKey: "checked") + } + alertController.addAction(myThreadsAction) + + alertController.addAction(UIAlertAction(title: VectorL10n.cancel, + style: .cancel, + handler: nil)) + + alertController.popoverPresentationController?.barButtonItem = navigationItem.rightBarButtonItem + + self.present(alertController, animated: true, completion: nil) + } + private func render(error: Error) { self.activityPresenter.removeCurrentActivityIndicator(animated: true) self.errorPresenter.presentError(from: self, forError: error, animated: true, handler: nil) @@ -155,7 +191,7 @@ final class ThreadListViewController: UIViewController { @objc private func filterButtonTapped(_ sender: UIBarButtonItem) { - self.viewModel.process(viewAction: .complete) + self.viewModel.process(viewAction: .showFilterTypes) } } diff --git a/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift b/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift index f8fa49806..ff4307af4 100644 --- a/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift +++ b/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift @@ -36,6 +36,7 @@ final class ThreadListViewModel: ThreadListViewModelProtocol { weak var viewDelegate: ThreadListViewModelViewDelegate? weak var coordinatorDelegate: ThreadListViewModelCoordinatorDelegate? + var selectedFilterType: ThreadListFilterType = .all private(set) var viewState: ThreadListViewState = .idle { didSet { @@ -49,6 +50,7 @@ final class ThreadListViewModel: ThreadListViewModelProtocol { roomId: String) { self.session = session self.roomId = roomId + session.threadingService.addDelegate(self) } deinit { @@ -60,12 +62,17 @@ final class ThreadListViewModel: ThreadListViewModelProtocol { func process(viewAction: ThreadListViewAction) { switch viewAction { case .loadData: - self.loadData() + loadData() + case .showFilterTypes: + viewState = .showingFilterTypes + case .selectFilterType(let type): + selectedFilterType = type + loadData() case .complete: - self.coordinatorDelegate?.threadListViewModelDidLoadThreads(self) + coordinatorDelegate?.threadListViewModelDidLoadThreads(self) case .cancel: - self.cancelOperations() - self.coordinatorDelegate?.threadListViewModelDidCancel(self) + cancelOperations() + coordinatorDelegate?.threadListViewModelDidCancel(self) } } @@ -166,8 +173,13 @@ final class ThreadListViewModel: ThreadListViewModelProtocol { viewState = .loading - threads = session.threadingService.threads(inRoom: roomId) - session.threadingService.addDelegate(self) + switch selectedFilterType { + case .all: + threads = session.threadingService.threads(inRoom: roomId) + case .myThreads: + threads = session.threadingService.participatedThreads(inRoom: roomId) + } + threadsLoaded() } diff --git a/Riot/Modules/Threads/ThreadList/ThreadListViewModelProtocol.swift b/Riot/Modules/Threads/ThreadList/ThreadListViewModelProtocol.swift index 1b8ea9024..6560a18ac 100644 --- a/Riot/Modules/Threads/ThreadList/ThreadListViewModelProtocol.swift +++ b/Riot/Modules/Threads/ThreadList/ThreadListViewModelProtocol.swift @@ -37,6 +37,21 @@ protocol ThreadListViewModelProtocol { var viewState: ThreadListViewState { get } + var selectedFilterType: ThreadListFilterType { get } var numberOfThreads: Int { get } func threadViewModel(at index: Int) -> ThreadViewModel? } + +enum ThreadListFilterType { + case all + case myThreads + + var title: String { + switch self { + case .all: + return VectorL10n.threadsActionAllThreads + case .myThreads: + return VectorL10n.threadsActionMyThreads + } + } +} diff --git a/Riot/Modules/Threads/ThreadList/ThreadListViewState.swift b/Riot/Modules/Threads/ThreadList/ThreadListViewState.swift index 8d7a742d2..17597d2a1 100644 --- a/Riot/Modules/Threads/ThreadList/ThreadListViewState.swift +++ b/Riot/Modules/Threads/ThreadList/ThreadListViewState.swift @@ -23,5 +23,6 @@ enum ThreadListViewState { case idle case loading case loaded + case showingFilterTypes case error(Error) } From 8c7dd6ae65ba6d777b36e7fbc827318e52ef0de0 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Fri, 19 Nov 2021 14:19:23 +0300 Subject: [PATCH 42/87] Push thread view instead of presenting --- .../Room/RoomCoordinatorBridgePresenter.swift | 17 -------- Riot/Modules/Room/RoomViewController.m | 11 +---- .../Title/Thread/ThreadRoomTitleView.swift | 36 +++++++++++------ .../Title/Thread/ThreadRoomTitleView.xib | 40 +++++++++---------- .../Modules/Thread/ThreadViewController.swift | 38 ------------------ 5 files changed, 43 insertions(+), 99 deletions(-) diff --git a/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift b/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift index 6184026a2..84c756780 100644 --- a/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift +++ b/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift @@ -85,23 +85,6 @@ final class RoomCoordinatorBridgePresenter: NSObject { // MARK: - Public func present(from viewController: UIViewController, animated: Bool) { - - if bridgeParameters.threadId != nil { - let coordinator = self.createRoomCoordinator() - coordinator.delegate = self - coordinator.start() - let presentable = coordinator.toPresentable() - - let presentationController = CustomSizedPresentationController(presentedViewController: presentable, - presenting: viewController) - presentationController.dismissOnBackgroundTap = false - presentationController.cornerRadius = 12 - presentable.transitioningDelegate = presentationController - viewController.present(presentable, animated: animated, completion: nil) - - self.coordinator = coordinator - return - } let coordinator = self.createRoomCoordinator() coordinator.delegate = self let presentable = coordinator.toPresentable() diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 02ba9a4df..73a3fc10d 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -4602,15 +4602,6 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; { [self declineRoomInvitation]; } - else if ([titleView isKindOfClass:ThreadRoomTitleView.class]) - { - ThreadRoomTitleView *threadTitleView = (ThreadRoomTitleView *)titleView; - if (tappedView == threadTitleView.closeButton) - { - // dismiss self - [self dismissViewControllerAnimated:YES completion:nil]; - } - } } - (void)declineRoomInvitation @@ -6303,7 +6294,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; displayConfiguration:configuration previewData:nil]; self.threadBridgePresenter = [[RoomCoordinatorBridgePresenter alloc] initWithParameters:parameters]; - [self.threadBridgePresenter presentFrom:self animated:YES]; + [self.threadBridgePresenter pushFrom:self.navigationController animated:YES]; } #pragma mark - RoomContextualMenuViewControllerDelegate diff --git a/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.swift b/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.swift index 83be5905f..21aaea539 100644 --- a/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.swift +++ b/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.swift @@ -43,16 +43,12 @@ class ThreadRoomTitleView: RoomTitleView { // Individual views @IBOutlet private weak var partialTitleLabel: UILabel! - @IBOutlet private weak var fullCloseButton: UIButton! @IBOutlet private weak var fullTitleLabel: UILabel! @IBOutlet private weak var fullRoomAvatarView: RoomAvatarView! + @IBOutlet private weak var fullRoomEncryptionBadgeView: UIImageView! @IBOutlet private weak var fullRoomNameLabel: UILabel! @IBOutlet private weak var fullOptionsButton: UIButton! - var closeButton: UIButton { - return fullCloseButton - } - override var mxRoom: MXRoom! { didSet { updateMode() @@ -81,12 +77,24 @@ class ThreadRoomTitleView: RoomTitleView { fallbackImage: AvatarFallbackImage.matrixItem(room.matrixItemId, room.displayName)) fullRoomAvatarView.fill(with: avatarViewData) + + guard let summary = room.summary else { + fullRoomEncryptionBadgeView.isHidden = true + return + } + if summary.isEncrypted && room.mxSession.crypto != nil { + fullRoomEncryptionBadgeView.image = EncryptionTrustLevelBadgeImageHelper.roomBadgeImage(for: summary.roomEncryptionTrustLevel()) + fullRoomEncryptionBadgeView.isHidden = false + } else { + fullRoomEncryptionBadgeView.isHidden = true + } } override func awakeFromNib() { super.awakeFromNib() update(theme: ThemeService.shared().theme) + registerThemeServiceDidChangeThemeNotification() } override func didMoveToSuperview() { @@ -101,6 +109,13 @@ class ThreadRoomTitleView: RoomTitleView { } } + private func registerThemeServiceDidChangeThemeNotification() { + NotificationCenter.default.addObserver(self, + selector: #selector(themeDidChange), + name: .themeServiceDidChangeTheme, + object: nil) + } + private func updateMode() { // ensure both mxRoom and threadId are set guard let room = mxRoom, @@ -133,11 +148,8 @@ class ThreadRoomTitleView: RoomTitleView { // MARK: - Actions - @IBAction private func closeButtonTapped(_ sender: UIButton) { - let gesture = UITapGestureRecognizer(target: nil, action: nil) - closeButton.addGestureRecognizer(gesture) - tapGestureDelegate.roomTitleView(self, recognizeTapGesture: gesture) - closeButton.removeGestureRecognizer(gesture) + @objc private func themeDidChange() { + self.update(theme: ThemeService.shared().theme) } @IBAction private func optionsButtonTapped(_ sender: UIButton) { @@ -150,10 +162,10 @@ extension ThreadRoomTitleView: Themable { func update(theme: Theme) { partialTitleLabel.textColor = theme.colors.primaryContent - fullCloseButton.tintColor = theme.colors.secondaryContent + fullRoomAvatarView.backgroundColor = .clear fullTitleLabel.textColor = theme.colors.primaryContent fullRoomNameLabel.textColor = theme.colors.secondaryContent - fullOptionsButton.tintColor = theme.colors.secondaryContent + fullOptionsButton.tintColor = theme.colors.accent } } diff --git a/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.xib b/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.xib index c47ecc287..53262fb17 100644 --- a/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.xib +++ b/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.xib @@ -34,33 +34,30 @@ - - - + + + + + + + + + + @@ -103,10 +100,10 @@ - + @@ -116,7 +113,6 @@ - diff --git a/Riot/Modules/Thread/ThreadViewController.swift b/Riot/Modules/Thread/ThreadViewController.swift index a6df5cc6a..9e803baf5 100644 --- a/Riot/Modules/Thread/ThreadViewController.swift +++ b/Riot/Modules/Thread/ThreadViewController.swift @@ -20,11 +20,6 @@ class ThreadViewController: RoomViewController { // MARK: Private - private enum Constants { - static let sizeOniPad: CGSize = CGSize(width: 375, height: 667) - static let additionalTopInset: CGFloat = 20 - } - private(set) var threadId: String! class func instantiate(withThreadId threadId: String, @@ -49,37 +44,4 @@ class ThreadViewController: RoomViewController { threadTitleView.threadId = threadId } - private func topSafeAreaInset() -> CGFloat { - guard let window = UIApplication.shared.keyWindow else { - return Constants.additionalTopInset - } - - return window.safeAreaInsets.top + Constants.additionalTopInset - } - -} - -// MARK: - CustomSizedPresentable - -extension ThreadViewController: CustomSizedPresentable { - - func customSize(withParentContainerSize containerSize: CGSize) -> CGSize { - if UIDevice.current.isPhone { - return CGSize(width: containerSize.width, - height: containerSize.height - topSafeAreaInset()) - } - return Constants.sizeOniPad - } - - func position(withParentContainerSize containerSize: CGSize) -> CGPoint { - let mySize = customSize(withParentContainerSize: containerSize) - - if UIDevice.current.isPhone { - return CGPoint(x: 0, y: topSafeAreaInset()) - } - - return CGPoint(x: (containerSize.width - mySize.width)/2, - y: (containerSize.height - mySize.height)/2) - } - } From c4f996d10fa3a809435c7de4f06136501d2c9191 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Fri, 19 Nov 2021 14:35:46 +0300 Subject: [PATCH 43/87] Allow dismissal of pushed coordinator --- .../Room/RoomCoordinatorBridgePresenter.swift | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift b/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift index 6a82e835f..b741d704e 100644 --- a/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift +++ b/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift @@ -72,6 +72,12 @@ final class RoomCoordinatorBridgePresenter: NSObject { private let bridgeParameters: RoomCoordinatorBridgePresenterParameters private var coordinator: RoomCoordinator? + private var navigationType: NavigationType = .present + + private enum NavigationType { + case present + case push + } // MARK: Public @@ -95,6 +101,7 @@ final class RoomCoordinatorBridgePresenter: NSObject { coordinator.start() self.coordinator = coordinator + self.navigationType = .present } func push(from navigationController: UINavigationController, animated: Bool) { @@ -106,13 +113,25 @@ final class RoomCoordinatorBridgePresenter: NSObject { coordinator.start() // Will trigger view controller push self.coordinator = coordinator + self.navigationType = .push } func dismiss(animated: Bool, completion: (() -> Void)?) { guard let coordinator = self.coordinator else { return } - coordinator.toPresentable().dismiss(animated: animated) { + switch navigationType { + case .present: + coordinator.toPresentable().dismiss(animated: animated) { + self.coordinator = nil + + completion?() + } + case .push: + guard let navigationController = coordinator.toPresentable() as? UINavigationController else { + return + } + navigationController.popViewController(animated: animated) self.coordinator = nil completion?() From c6823624e98251e840ec4dc267e4bcbae0191c88 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Fri, 19 Nov 2021 14:37:09 +0300 Subject: [PATCH 44/87] Move view in room action to the top most --- Riot/Modules/Room/RoomViewController.m | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index b76d7f4f5..e80808d5e 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -3266,15 +3266,6 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; }]]; } - if (selectedEvent.sentState == MXEventSentStateSent) { - [actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionForward] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - MXStrongifyAndReturnIfNil(self); - [self presentEventForwardingDialogForSelectedEvent:selectedEvent]; - }]]; - } - if (self.roomDataSource.threadId && [selectedEvent.eventId isEqualToString:self.roomDataSource.threadId]) { // if in the thread and selected event is the root event @@ -3289,6 +3280,15 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; }]]; } + if (selectedEvent.sentState == MXEventSentStateSent) { + [actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionForward] + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { + MXStrongifyAndReturnIfNil(self); + [self presentEventForwardingDialogForSelectedEvent:selectedEvent]; + }]]; + } + if (!isJitsiCallEvent) { [actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionQuote] From 6a35e0c8f8e5325ca737f24b3cbab4a80c848771 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Fri, 19 Nov 2021 14:43:38 +0300 Subject: [PATCH 45/87] Fix alert --- Riot/Modules/Room/RoomViewController.m | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index e80808d5e..bc12ef2d1 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -3270,9 +3270,9 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; { // if in the thread and selected event is the root event // add "View in room" action - [currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionViewInRoom] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { + [actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionViewInRoom] + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { MXStrongifyAndReturnIfNil(self); [self.delegate roomViewController:self showRoomWithId:self.roomDataSource.roomId From 2fc2d916b192179e5c385bbc8266dad04d92ac1d Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Fri, 19 Nov 2021 16:31:18 +0300 Subject: [PATCH 46/87] Add filter icon --- .../Room/Threads/Contents.json | 6 +++++ .../threads_filter.imageset/Contents.json | 23 ++++++++++++++++++ .../filter_list_black_24dp 1.png | Bin 0 -> 204 bytes .../filter_list_black_24dp 1@2x.png | Bin 0 -> 295 bytes .../filter_list_black_24dp 1@3x.png | Bin 0 -> 377 bytes Riot/Generated/Images.swift | 1 + 6 files changed, 30 insertions(+) create mode 100644 Riot/Assets/Images.xcassets/Room/Threads/Contents.json create mode 100644 Riot/Assets/Images.xcassets/Room/Threads/threads_filter.imageset/Contents.json create mode 100644 Riot/Assets/Images.xcassets/Room/Threads/threads_filter.imageset/filter_list_black_24dp 1.png create mode 100644 Riot/Assets/Images.xcassets/Room/Threads/threads_filter.imageset/filter_list_black_24dp 1@2x.png create mode 100644 Riot/Assets/Images.xcassets/Room/Threads/threads_filter.imageset/filter_list_black_24dp 1@3x.png diff --git a/Riot/Assets/Images.xcassets/Room/Threads/Contents.json b/Riot/Assets/Images.xcassets/Room/Threads/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Threads/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Threads/threads_filter.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Threads/threads_filter.imageset/Contents.json new file mode 100644 index 000000000..7c7666862 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Threads/threads_filter.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "filter_list_black_24dp 1.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "filter_list_black_24dp 1@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "filter_list_black_24dp 1@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Threads/threads_filter.imageset/filter_list_black_24dp 1.png b/Riot/Assets/Images.xcassets/Room/Threads/threads_filter.imageset/filter_list_black_24dp 1.png new file mode 100644 index 0000000000000000000000000000000000000000..7710fa90e568b82f4ba1fd3048232151d25b256a GIT binary patch literal 204 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjoCO|{#S9GG!XV7ZFl!D-1!HlL zyA#8@b22Z19F}xPUq=Rpjs4tz5?O)#L{AsTkP61P7dP@YCxi{N>!uvNVKMcS1~i=qs^xh$@g4iRPP`jE^Bw2Z;i)z4*}Q$iB}y{<@T literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Room/Threads/threads_filter.imageset/filter_list_black_24dp 1@2x.png b/Riot/Assets/Images.xcassets/Room/Threads/threads_filter.imageset/filter_list_black_24dp 1@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..b9cff9c1be71b3df4428ae3507f9e3fc2bf9200e GIT binary patch literal 295 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTC&H|6fVg?2=RS;(M3{v?36l5$8 za(7}_cTVOdki(Mh=l_SQb3}@T zyQ7>x^PpFbsw#`kY||$9^l7o1R3?3Q^qbr|^YUb%)@`ddvh6o06M z|M=;@Y>6}J$vNR0{v1!dwRCga!O9(NPxn7c@SVf_sPb2pZvF-DaK-~#rZfLv*Vz&s z;~lWBs!PC{xWt~$(696s#aCra# literal 0 HcmV?d00001 diff --git a/Riot/Assets/Images.xcassets/Room/Threads/threads_filter.imageset/filter_list_black_24dp 1@3x.png b/Riot/Assets/Images.xcassets/Room/Threads/threads_filter.imageset/filter_list_black_24dp 1@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..5fec0029d0bcedf04bcd3c775960386c6dfe28dd GIT binary patch literal 377 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@ZgvM!oCO|{#S9FJ79h;%I?XTvD9BhG z<_9J&ZiAUSd0U#p4$Y z90H0Hn1n7cera^^<`ArI;QFI4wQ*Zz{F8(y345a|=Su=j1A!HjtSok{5swQOHvYfW z&-;2x-JZW^mtERoWU?ctEbjJ^sw1zfO6AW@_br~vIj`Gz(>%{m)39etSbwFx; Date: Fri, 19 Nov 2021 16:32:20 +0300 Subject: [PATCH 47/87] Introduce view model for thread title view --- .../Thread/ThreadRoomTitleViewModel.swift | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleViewModel.swift diff --git a/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleViewModel.swift b/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleViewModel.swift new file mode 100644 index 000000000..9db319dea --- /dev/null +++ b/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleViewModel.swift @@ -0,0 +1,27 @@ +// +// 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 Foundation + +struct ThreadRoomTitleViewModel { + var roomAvatar: AvatarViewDataProtocol? + var roomEncryptionBadge: UIImage? + var roomDisplayName: String? + + static let empty = ThreadRoomTitleViewModel(roomAvatar: nil, + roomEncryptionBadge: nil, + roomDisplayName: nil) +} From 308c7bdfc8a308f8842ef0fc9540a700d1b6fa0c Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Fri, 19 Nov 2021 16:32:42 +0300 Subject: [PATCH 48/87] Refactor title view --- .../Title/Thread/ThreadRoomTitleView.swift | 117 ++++++++---------- .../Title/Thread/ThreadRoomTitleView.xib | 47 ++----- .../Threads/Thread/ThreadViewController.swift | 2 +- 3 files changed, 65 insertions(+), 101 deletions(-) diff --git a/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.swift b/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.swift index 21aaea539..d3d387a6a 100644 --- a/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.swift +++ b/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.swift @@ -16,59 +16,64 @@ import Foundation import MatrixKit +import Reusable + +enum ThreadRoomTitleViewMode { + case allThreads + case specificThread(threadId: String) +} @objc -enum ThreadRoomTitleViewMode: Int { - case partial - case full +protocol ThreadRoomTitleViewDelegate: AnyObject { + func threadRoomTitleViewDidTapOptions(_ view: ThreadRoomTitleView) } @objcMembers class ThreadRoomTitleView: RoomTitleView { - var mode: ThreadRoomTitleViewMode = .full { + var mode: ThreadRoomTitleViewMode = .allThreads { didSet { update() } } - var threadId: String! { - didSet { - updateMode() + weak var viewDelegate: ThreadRoomTitleViewDelegate? + + @IBOutlet private weak var titleLabel: UILabel! + @IBOutlet private weak var roomAvatarView: RoomAvatarView! + @IBOutlet private weak var roomEncryptionBadgeView: UIImageView! + @IBOutlet private weak var roomNameLabel: UILabel! + @IBOutlet private weak var optionsButton: UIButton! + + // MARK: - Methods + + func configure(withViewModel viewModel: ThreadRoomTitleViewModel) { + if let avatarViewData = viewModel.roomAvatar { + roomAvatarView.fill(with: avatarViewData) + } else { + roomAvatarView.avatarImageView.image = nil } + roomEncryptionBadgeView.image = viewModel.roomEncryptionBadge + roomEncryptionBadgeView.isHidden = viewModel.roomEncryptionBadge == nil + roomNameLabel.text = viewModel.roomDisplayName } - // Container views - @IBOutlet private weak var partialContainerView: UIView! - @IBOutlet private weak var fullContainerView: UIView! - - // Individual views - @IBOutlet private weak var partialTitleLabel: UILabel! - @IBOutlet private weak var fullTitleLabel: UILabel! - @IBOutlet private weak var fullRoomAvatarView: RoomAvatarView! - @IBOutlet private weak var fullRoomEncryptionBadgeView: UIImageView! - @IBOutlet private weak var fullRoomNameLabel: UILabel! - @IBOutlet private weak var fullOptionsButton: UIButton! + // MARK: - Overrides override var mxRoom: MXRoom! { didSet { - updateMode() + update() } } override class func nib() -> UINib! { - return UINib(nibName: String(describing: self), - bundle: .main) + return self.nib } override func refreshDisplay() { - partialTitleLabel.text = VectorL10n.roomThreadTitle - fullTitleLabel.text = VectorL10n.roomThreadTitle - guard let room = mxRoom else { // room not initialized yet return } - fullRoomNameLabel.text = room.displayName let avatarViewData = AvatarViewData(matrixItemId: room.matrixItemId, displayName: room.displayName, @@ -76,18 +81,18 @@ class ThreadRoomTitleView: RoomTitleView { mediaManager: room.mxSession.mediaManager, fallbackImage: AvatarFallbackImage.matrixItem(room.matrixItemId, room.displayName)) - fullRoomAvatarView.fill(with: avatarViewData) - guard let summary = room.summary else { - fullRoomEncryptionBadgeView.isHidden = true - return - } - if summary.isEncrypted && room.mxSession.crypto != nil { - fullRoomEncryptionBadgeView.image = EncryptionTrustLevelBadgeImageHelper.roomBadgeImage(for: summary.roomEncryptionTrustLevel()) - fullRoomEncryptionBadgeView.isHidden = false + let encrpytionBadge: UIImage? + if let summary = room.summary, room.mxSession.crypto != nil { + encrpytionBadge = EncryptionTrustLevelBadgeImageHelper.roomBadgeImage(for: summary.roomEncryptionTrustLevel()) } else { - fullRoomEncryptionBadgeView.isHidden = true + encrpytionBadge = nil } + + let viewModel = ThreadRoomTitleViewModel(roomAvatar: avatarViewData, + roomEncryptionBadge: encrpytionBadge, + roomDisplayName: room.displayName) + configure(withViewModel: viewModel) } override func awakeFromNib() { @@ -116,33 +121,14 @@ class ThreadRoomTitleView: RoomTitleView { object: nil) } - private func updateMode() { - // ensure both mxRoom and threadId are set - guard let room = mxRoom, - let threadId = threadId else { - return - } - - if room.mxSession.threadingService.thread(withId: threadId) == nil { - // thread not created yet - mode = .partial - // use full mode for every case for now - // TODO: Fix in future - mode = .full - } else { - // thread created before - mode = .full - } - } - private func update() { switch mode { - case .partial: - partialContainerView.isHidden = false - fullContainerView.isHidden = true - case .full: - partialContainerView.isHidden = true - fullContainerView.isHidden = false + case .allThreads: + titleLabel.text = VectorL10n.threadsTitle + optionsButton.setImage(Asset.Images.threadsFilter.image, for: .normal) + case .specificThread: + titleLabel.text = VectorL10n.roomThreadTitle + optionsButton.setImage(Asset.Images.roomContextMenuMore.image, for: .normal) } } @@ -153,19 +139,20 @@ class ThreadRoomTitleView: RoomTitleView { } @IBAction private func optionsButtonTapped(_ sender: UIButton) { - + viewDelegate?.threadRoomTitleViewDidTapOptions(self) } } +extension ThreadRoomTitleView: NibLoadable {} + extension ThreadRoomTitleView: Themable { func update(theme: Theme) { - partialTitleLabel.textColor = theme.colors.primaryContent - fullRoomAvatarView.backgroundColor = .clear - fullTitleLabel.textColor = theme.colors.primaryContent - fullRoomNameLabel.textColor = theme.colors.secondaryContent - fullOptionsButton.tintColor = theme.colors.accent + roomAvatarView.backgroundColor = .clear + titleLabel.textColor = theme.colors.primaryContent + roomNameLabel.textColor = theme.colors.secondaryContent + optionsButton.tintColor = theme.colors.accent } } diff --git a/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.xib b/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.xib index 53262fb17..07a21fbb2 100644 --- a/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.xib +++ b/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.xib @@ -15,33 +15,17 @@ - - - - + - @@ -101,19 +91,18 @@ - + - From 8e303788c1a23b73a0b82cc614a8d05f723849e1 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Mon, 22 Nov 2021 16:16:15 +0300 Subject: [PATCH 54/87] Add thread list empty view --- Riot/Assets/en.lproj/Vector.strings | 5 + Riot/Generated/Strings.swift | 20 ++++ .../ThreadListViewController.storyboard | 14 ++- .../ThreadList/ThreadListViewController.swift | 27 +++++ .../ThreadList/ThreadListViewModel.swift | 34 +++++- .../ThreadList/ThreadListViewState.swift | 1 + .../{ => Cell}/ThreadTableViewCell.swift | 0 .../Views/{ => Cell}/ThreadTableViewCell.xib | 0 .../Views/{ => Cell}/ThreadViewModel.swift | 0 .../Views/Empty/ThreadListEmptyView.swift | 73 ++++++++++++ .../Views/Empty/ThreadListEmptyView.xib | 110 ++++++++++++++++++ .../Empty/ThreadListEmptyViewModel.swift | 26 +++++ 12 files changed, 305 insertions(+), 5 deletions(-) rename Riot/Modules/Threads/ThreadList/Views/{ => Cell}/ThreadTableViewCell.swift (100%) rename Riot/Modules/Threads/ThreadList/Views/{ => Cell}/ThreadTableViewCell.xib (100%) rename Riot/Modules/Threads/ThreadList/Views/{ => Cell}/ThreadViewModel.swift (100%) create mode 100644 Riot/Modules/Threads/ThreadList/Views/Empty/ThreadListEmptyView.swift create mode 100644 Riot/Modules/Threads/ThreadList/Views/Empty/ThreadListEmptyView.xib create mode 100644 Riot/Modules/Threads/ThreadList/Views/Empty/ThreadListEmptyViewModel.swift diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 3386d9128..459c80331 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -422,6 +422,11 @@ Tap the + to start adding people."; "threads_title" = "Threads"; "threads_action_all_threads" = "All threads"; "threads_action_my_threads" = "My threads"; +"threads_empty_title" = "Keep discussions organised with threads"; +"threads_empty_info_all" = "Threads help keep your conversations on-topic and easy to track."; +"threads_empty_info_my" = "Reply to an ongoing thread or use “Thread” when selecting a message to start a new one."; +"threads_empty_tip" = "Tip: Use “Thread” option when selecting a message."; +"threads_empty_show_all_threads" = "Show all threads"; "media_type_accessibility_image" = "Image"; "media_type_accessibility_audio" = "Audio"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index d7ec3f082..36a213c7d 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -5079,6 +5079,26 @@ public class VectorL10n: NSObject { public static var threadsActionMyThreads: String { return VectorL10n.tr("Vector", "threads_action_my_threads") } + /// Threads help keep your conversations on-topic and easy to track. + public static var threadsEmptyInfoAll: String { + return VectorL10n.tr("Vector", "threads_empty_info_all") + } + /// Reply to an ongoing thread or use “Thread” when selecting a message to start a new one. + public static var threadsEmptyInfoMy: String { + return VectorL10n.tr("Vector", "threads_empty_info_my") + } + /// Show all threads + public static var threadsEmptyShowAllThreads: String { + return VectorL10n.tr("Vector", "threads_empty_show_all_threads") + } + /// Tip: Use “Thread” option when selecting a message. + public static var threadsEmptyTip: String { + return VectorL10n.tr("Vector", "threads_empty_tip") + } + /// Keep discussions organised with threads + public static var threadsEmptyTitle: String { + return VectorL10n.tr("Vector", "threads_empty_title") + } /// Threads public static var threadsTitle: String { return VectorL10n.tr("Vector", "threads_title") diff --git a/Riot/Modules/Threads/ThreadList/ThreadListViewController.storyboard b/Riot/Modules/Threads/ThreadList/ThreadListViewController.storyboard index 696d736bf..ddfb9060c 100644 --- a/Riot/Modules/Threads/ThreadList/ThreadListViewController.storyboard +++ b/Riot/Modules/Threads/ThreadList/ThreadListViewController.storyboard @@ -25,23 +25,35 @@ + + + + + + + + + + + + - + diff --git a/Riot/Modules/Threads/ThreadList/ThreadListViewController.swift b/Riot/Modules/Threads/ThreadList/ThreadListViewController.swift index 8bb5fe9b4..d027e6209 100644 --- a/Riot/Modules/Threads/ThreadList/ThreadListViewController.swift +++ b/Riot/Modules/Threads/ThreadList/ThreadListViewController.swift @@ -31,6 +31,7 @@ final class ThreadListViewController: UIViewController { // MARK: Outlets @IBOutlet private weak var threadsTableView: UITableView! + @IBOutlet private weak var emptyView: ThreadListEmptyView! // MARK: Private @@ -102,6 +103,8 @@ final class ThreadListViewController: UIViewController { theme.applyStyle(onNavigationBar: navigationBar) } + emptyView.update(theme: theme) + emptyView.backgroundColor = theme.colors.background self.threadsTableView.backgroundColor = theme.backgroundColor self.threadsTableView.separatorColor = theme.colors.separator self.threadsTableView.reloadData() @@ -142,6 +145,8 @@ final class ThreadListViewController: UIViewController { self.renderLoading() case .loaded: self.renderLoaded() + case .empty(let viewModel): + self.renderEmptyView(withViewModel: viewModel) case .showingFilterTypes: self.renderShowingFilterTypes() case .error(let error): @@ -150,12 +155,24 @@ final class ThreadListViewController: UIViewController { } private func renderLoading() { + emptyView.isHidden = true + threadsTableView.isHidden = true self.activityPresenter.presentActivityIndicator(on: self.view, animated: true) } private func renderLoaded() { self.activityPresenter.removeCurrentActivityIndicator(animated: true) + threadsTableView.isHidden = false self.threadsTableView.reloadData() + navigationItem.rightBarButtonItem?.isEnabled = true + } + + private func renderEmptyView(withViewModel emptyViewModel: ThreadListEmptyViewModel) { + self.activityPresenter.removeCurrentActivityIndicator(animated: true) + emptyView.configure(withViewModel: emptyViewModel) + threadsTableView.isHidden = true + emptyView.isHidden = false + navigationItem.rightBarButtonItem?.isEnabled = viewModel.selectedFilterType == .myThreads } private func renderShowingFilterTypes() { @@ -254,3 +271,13 @@ extension ThreadListViewController: UITableViewDelegate { } } + +// MARK: - ThreadListEmptyViewDelegate + +extension ThreadListViewController: ThreadListEmptyViewDelegate { + + func threadListEmptyViewTappedShowAllThreads(_ emptyView: ThreadListEmptyView) { + viewModel.process(viewAction: .selectFilterType(.all)) + } + +} diff --git a/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift b/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift index 57c36f36c..1e980520d 100644 --- a/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift +++ b/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift @@ -54,6 +54,7 @@ final class ThreadListViewModel: ThreadListViewModelProtocol { } deinit { + session.threadingService.removeDelegate(self) self.cancelOperations() } @@ -113,6 +114,25 @@ final class ThreadListViewModel: ThreadListViewModelProtocol { roomDisplayName: room.displayName) } + private var emptyViewModel: ThreadListEmptyViewModel { + switch selectedFilterType { + case .all: + return ThreadListEmptyViewModel(icon: Asset.Images.roomContextMenuReplyInThread.image, + title: VectorL10n.threadsEmptyTitle, + info: VectorL10n.threadsEmptyInfoAll, + tip: VectorL10n.threadsEmptyTip, + showAllThreadsButtonTitle: VectorL10n.threadsEmptyShowAllThreads, + showAllThreadsButtonHidden: true) + case .myThreads: + return ThreadListEmptyViewModel(icon: Asset.Images.roomContextMenuReplyInThread.image, + title: VectorL10n.threadsEmptyTitle, + info: VectorL10n.threadsEmptyInfoMy, + tip: VectorL10n.threadsEmptyTip, + showAllThreadsButtonTitle: VectorL10n.threadsEmptyShowAllThreads, + showAllThreadsButtonHidden: false) + } + } + // MARK: - Private private func viewModel(forThread thread: MXThread) -> ThreadViewModel { @@ -195,9 +215,11 @@ final class ThreadListViewModel: ThreadListViewModelProtocol { ) } - private func loadData() { + private func loadData(showLoading: Bool = true) { - viewState = .loading + if showLoading { + viewState = .loading + } switch selectedFilterType { case .all: @@ -206,6 +228,11 @@ final class ThreadListViewModel: ThreadListViewModelProtocol { threads = session.threadingService.participatedThreads(inRoom: roomId) } + if threads.isEmpty { + viewState = .empty(emptyViewModel) + return + } + threadsLoaded() } @@ -244,8 +271,7 @@ final class ThreadListViewModel: ThreadListViewModelProtocol { extension ThreadListViewModel: MXThreadingServiceDelegate { func threadingServiceDidUpdateThreads(_ service: MXThreadingService) { - threads = service.threads(inRoom: roomId) - viewState = .loaded + loadData(showLoading: false) } } diff --git a/Riot/Modules/Threads/ThreadList/ThreadListViewState.swift b/Riot/Modules/Threads/ThreadList/ThreadListViewState.swift index 17597d2a1..073c70645 100644 --- a/Riot/Modules/Threads/ThreadList/ThreadListViewState.swift +++ b/Riot/Modules/Threads/ThreadList/ThreadListViewState.swift @@ -23,6 +23,7 @@ enum ThreadListViewState { case idle case loading case loaded + case empty(_ viewModel: ThreadListEmptyViewModel) case showingFilterTypes case error(Error) } diff --git a/Riot/Modules/Threads/ThreadList/Views/ThreadTableViewCell.swift b/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.swift similarity index 100% rename from Riot/Modules/Threads/ThreadList/Views/ThreadTableViewCell.swift rename to Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.swift diff --git a/Riot/Modules/Threads/ThreadList/Views/ThreadTableViewCell.xib b/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.xib similarity index 100% rename from Riot/Modules/Threads/ThreadList/Views/ThreadTableViewCell.xib rename to Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.xib diff --git a/Riot/Modules/Threads/ThreadList/Views/ThreadViewModel.swift b/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadViewModel.swift similarity index 100% rename from Riot/Modules/Threads/ThreadList/Views/ThreadViewModel.swift rename to Riot/Modules/Threads/ThreadList/Views/Cell/ThreadViewModel.swift diff --git a/Riot/Modules/Threads/ThreadList/Views/Empty/ThreadListEmptyView.swift b/Riot/Modules/Threads/ThreadList/Views/Empty/ThreadListEmptyView.swift new file mode 100644 index 000000000..213f1fa18 --- /dev/null +++ b/Riot/Modules/Threads/ThreadList/Views/Empty/ThreadListEmptyView.swift @@ -0,0 +1,73 @@ +// +// 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 Foundation +import Reusable + +@objc +protocol ThreadListEmptyViewDelegate: AnyObject { + func threadListEmptyViewTappedShowAllThreads(_ emptyView: ThreadListEmptyView) +} + +class ThreadListEmptyView: UIView { + + @IBOutlet weak var delegate: ThreadListEmptyViewDelegate? + + @IBOutlet private weak var iconBackgroundView: UIView! + @IBOutlet private weak var iconView: UIImageView! + @IBOutlet private weak var titleLabel: UILabel! + @IBOutlet private weak var infoLabel: UILabel! + @IBOutlet private weak var tipLabel: UILabel! + @IBOutlet private weak var showAllThreadsButton: UIButton! + + required init?(coder: NSCoder) { + super.init(coder: coder) + loadNibContent() + } + + func configure(withViewModel viewModel: ThreadListEmptyViewModel) { + iconView.image = viewModel.icon + titleLabel.text = viewModel.title + infoLabel.text = viewModel.info + tipLabel.text = viewModel.tip + showAllThreadsButton.setTitle(viewModel.showAllThreadsButtonTitle, + for: .normal) + showAllThreadsButton.isHidden = viewModel.showAllThreadsButtonHidden + + titleLabel.isHidden = titleLabel.text?.isEmpty ?? true + infoLabel.isHidden = infoLabel.text?.isEmpty ?? true + tipLabel.isHidden = tipLabel.text?.isEmpty ?? true + } + + @IBAction private func showAllThreadsButtonTapped(_ sender: UIButton) { + delegate?.threadListEmptyViewTappedShowAllThreads(self) + } + +} + +extension ThreadListEmptyView: NibOwnerLoadable {} + +extension ThreadListEmptyView: Themable { + + func update(theme: Theme) { + iconBackgroundView.backgroundColor = theme.colors.system + titleLabel.textColor = theme.colors.primaryContent + infoLabel.textColor = theme.colors.secondaryContent + tipLabel.textColor = theme.colors.secondaryContent + showAllThreadsButton.tintColor = theme.colors.accent + } + +} diff --git a/Riot/Modules/Threads/ThreadList/Views/Empty/ThreadListEmptyView.xib b/Riot/Modules/Threads/ThreadList/Views/Empty/ThreadListEmptyView.xib new file mode 100644 index 000000000..50c62af99 --- /dev/null +++ b/Riot/Modules/Threads/ThreadList/Views/Empty/ThreadListEmptyView.xib @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Threads/ThreadList/Views/Empty/ThreadListEmptyViewModel.swift b/Riot/Modules/Threads/ThreadList/Views/Empty/ThreadListEmptyViewModel.swift new file mode 100644 index 000000000..bef214bba --- /dev/null +++ b/Riot/Modules/Threads/ThreadList/Views/Empty/ThreadListEmptyViewModel.swift @@ -0,0 +1,26 @@ +// +// 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 Foundation + +struct ThreadListEmptyViewModel { + let icon: UIImage? + let title: String? + let info: String? + let tip: String? + let showAllThreadsButtonTitle: String? + let showAllThreadsButtonHidden: Bool +} From 55b752f198f31f45745832abaa06eed9d699cf7b Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Mon, 22 Nov 2021 17:36:28 +0300 Subject: [PATCH 55/87] Fix view in room thread action when navigated from thread list --- .../Room/RoomCoordinatorBridgePresenter.swift | 2 +- Riot/Modules/Room/RoomViewController.m | 73 +++++++++++++------ Riot/Modules/Threads/ThreadsCoordinator.swift | 19 ++++- .../ThreadsCoordinatorBridgePresenter.swift | 22 +++--- .../Threads/ThreadsCoordinatorProtocol.swift | 2 + 5 files changed, 82 insertions(+), 36 deletions(-) diff --git a/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift b/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift index b741d704e..bd6b70d77 100644 --- a/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift +++ b/Riot/Modules/Room/RoomCoordinatorBridgePresenter.swift @@ -128,7 +128,7 @@ final class RoomCoordinatorBridgePresenter: NSObject { completion?() } case .push: - guard let navigationController = coordinator.toPresentable() as? UINavigationController else { + guard let navigationController = coordinator.toPresentable().navigationController else { return } navigationController.popViewController(animated: animated) diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 3e492e28c..7aebb0d2c 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -140,7 +140,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; @interface RoomViewController () + RoomDataSourceDelegate, RoomCreationModalCoordinatorBridgePresenterDelegate, RoomInfoCoordinatorBridgePresenterDelegate, DialpadViewControllerDelegate, RemoveJitsiWidgetViewDelegate, VoiceMessageControllerDelegate, SpaceDetailPresenterDelegate, UserSuggestionCoordinatorBridgeDelegate, RoomCoordinatorBridgePresenterDelegate, ThreadsCoordinatorBridgePresenterDelegate> { // The preview header @@ -4374,6 +4374,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; { self.threadsCoordinatorBridgePresenter = [[ThreadsCoordinatorBridgePresenter alloc] initWithSession:self.mainSession roomId:self.roomDataSource.roomId]; + self.threadsCoordinatorBridgePresenter.delegate = self; [self.threadsCoordinatorBridgePresenter pushFrom:self.navigationController animated:YES]; } @@ -6382,6 +6383,30 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [self.threadBridgePresenter pushFrom:self.navigationController animated:YES]; } +- (void)highlightEvent:(NSString *)eventId +{ + NSInteger row = [self.roomDataSource indexOfCellDataWithEventId:eventId]; + if (row == NSNotFound) + { + return; + } + + self->customizedRoomDataSource.highlightedEventId = eventId; + + NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:0]; + if ([[self.bubblesTableView indexPathsForVisibleRows] containsObject:indexPath]) + { + [self.bubblesTableView reloadRowsAtIndexPaths:@[indexPath] + withRowAnimation:UITableViewRowAnimationAutomatic]; + } + else if ([self.bubblesTableView vc_hasIndexPath:indexPath]) + { + [self.bubblesTableView scrollToRowAtIndexPath:indexPath + atScrollPosition:UITableViewScrollPositionMiddle + animated:YES]; + } +} + #pragma mark - RoomContextualMenuViewControllerDelegate - (void)roomContextualMenuViewControllerDidTapBackgroundOverlay:(RoomContextualMenuViewController *)viewController @@ -6805,26 +6830,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [self.threadBridgePresenter dismissWithAnimated:YES completion:^{ MXStrongifyAndReturnIfNil(self); - NSInteger row = [self.roomDataSource indexOfCellDataWithEventId:eventId]; - if (row == NSNotFound) - { - return; - } - - self->customizedRoomDataSource.highlightedEventId = eventId; - - NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:0]; - if ([[self.bubblesTableView indexPathsForVisibleRows] containsObject:indexPath]) - { - [self.bubblesTableView reloadRowsAtIndexPaths:@[indexPath] - withRowAnimation:UITableViewRowAnimationAutomatic]; - } - else if ([self.bubblesTableView vc_hasIndexPath:indexPath]) - { - [self.bubblesTableView scrollToRowAtIndexPath:indexPath - atScrollPosition:UITableViewScrollPositionMiddle - animated:YES]; - } + [self highlightEvent:eventId]; }]; } } @@ -6837,4 +6843,29 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; } } +#pragma mark - ThreadsCoordinatorBridgePresenterDelegate + +- (void)threadsCoordinatorBridgePresenterDelegateDidComplete:(ThreadsCoordinatorBridgePresenter *)coordinatorBridgePresenter +{ + self.threadsCoordinatorBridgePresenter = nil; +} + +- (void)threadsCoordinatorBridgePresenterDelegateDidSelect:(ThreadsCoordinatorBridgePresenter *)coordinatorBridgePresenter roomId:(NSString *)roomId eventId:(NSString *)eventId +{ + MXWeakify(self); + [self.threadsCoordinatorBridgePresenter dismissWithAnimated:YES completion:^{ + MXStrongifyAndReturnIfNil(self); + + if (eventId) + { + [self highlightEvent:eventId]; + } + }]; +} + +- (void)threadsCoordinatorBridgePresenterDidDismissInteractively:(ThreadsCoordinatorBridgePresenter *)coordinatorBridgePresenter +{ + self.threadsCoordinatorBridgePresenter = nil; +} + @end diff --git a/Riot/Modules/Threads/ThreadsCoordinator.swift b/Riot/Modules/Threads/ThreadsCoordinator.swift index e6624aafd..6b2cee444 100644 --- a/Riot/Modules/Threads/ThreadsCoordinator.swift +++ b/Riot/Modules/Threads/ThreadsCoordinator.swift @@ -72,7 +72,20 @@ final class ThreadsCoordinator: NSObject, ThreadsCoordinatorProtocol { self?.remove(childCoordinator: rootCoordinator) } } - } + } + + func stop() { + if selectedThreadCoordinator != nil { + let modules = self.navigationRouter.modules + guard modules.count >= 3 else { + return + } + let moduleToGoBack = modules[modules.count - 3] + self.navigationRouter.popToModule(moduleToGoBack, animated: true) + } else { + self.navigationRouter.popModule(animated: true) + } + } func toPresentable() -> UIViewController { return self.navigationRouter.toPresentable() @@ -128,7 +141,7 @@ extension ThreadsCoordinator: UIAdaptivePresentationControllerDelegate { // MARK: - ThreadListCoordinatorDelegate extension ThreadsCoordinator: ThreadListCoordinatorDelegate { func threadListCoordinatorDidLoadThreads(_ coordinator: ThreadListCoordinatorProtocol) { - self.delegate?.threadsCoordinatorDidComplete(self) + } func threadListCoordinatorDidSelectThread(_ coordinator: ThreadListCoordinatorProtocol, thread: MXThread) { @@ -156,7 +169,7 @@ extension ThreadsCoordinator: RoomCoordinatorDelegate { } func roomCoordinator(_ coordinator: RoomCoordinatorProtocol, didSelectRoomWithId roomId: String, eventId: String?) { - + self.delegate?.threadsCoordinatorDidSelect(self, roomId: roomId, eventId: eventId) } func roomCoordinatorDidDismissInteractively(_ coordinator: RoomCoordinatorProtocol) { diff --git a/Riot/Modules/Threads/ThreadsCoordinatorBridgePresenter.swift b/Riot/Modules/Threads/ThreadsCoordinatorBridgePresenter.swift index 6ec252055..6c12eb40c 100644 --- a/Riot/Modules/Threads/ThreadsCoordinatorBridgePresenter.swift +++ b/Riot/Modules/Threads/ThreadsCoordinatorBridgePresenter.swift @@ -20,6 +20,9 @@ import Foundation @objc protocol ThreadsCoordinatorBridgePresenterDelegate { func threadsCoordinatorBridgePresenterDelegateDidComplete(_ coordinatorBridgePresenter: ThreadsCoordinatorBridgePresenter) + func threadsCoordinatorBridgePresenterDelegateDidSelect(_ coordinatorBridgePresenter: ThreadsCoordinatorBridgePresenter, + roomId: String, + eventId: String?) func threadsCoordinatorBridgePresenterDidDismissInteractively(_ coordinatorBridgePresenter: ThreadsCoordinatorBridgePresenter) } @@ -107,21 +110,14 @@ final class ThreadsCoordinatorBridgePresenter: NSObject { coordinator.toPresentable().dismiss(animated: animated) { self.coordinator = nil - if let completion = completion { - completion() - } + completion?() } case .push: - // Pop view controller from UINavigationController - guard let navigationController = coordinator.toPresentable() as? UINavigationController else { - return - } - navigationController.popViewController(animated: animated) + // stop coordinator to pop modules as needed + coordinator.stop() self.coordinator = nil - if let completion = completion { - completion() - } + completion?() } } } @@ -133,6 +129,10 @@ extension ThreadsCoordinatorBridgePresenter: ThreadsCoordinatorDelegate { self.delegate?.threadsCoordinatorBridgePresenterDelegateDidComplete(self) } + func threadsCoordinatorDidSelect(_ coordinator: ThreadsCoordinatorProtocol, roomId: String, eventId: String?) { + self.delegate?.threadsCoordinatorBridgePresenterDelegateDidSelect(self, roomId: roomId, eventId: eventId) + } + func threadsCoordinatorDidDismissInteractively(_ coordinator: ThreadsCoordinatorProtocol) { self.delegate?.threadsCoordinatorBridgePresenterDidDismissInteractively(self) } diff --git a/Riot/Modules/Threads/ThreadsCoordinatorProtocol.swift b/Riot/Modules/Threads/ThreadsCoordinatorProtocol.swift index c342fa33d..69d6a90f2 100644 --- a/Riot/Modules/Threads/ThreadsCoordinatorProtocol.swift +++ b/Riot/Modules/Threads/ThreadsCoordinatorProtocol.swift @@ -21,6 +21,8 @@ import Foundation protocol ThreadsCoordinatorDelegate: AnyObject { func threadsCoordinatorDidComplete(_ coordinator: ThreadsCoordinatorProtocol) + func threadsCoordinatorDidSelect(_ coordinator: ThreadsCoordinatorProtocol, roomId: String, eventId: String?) + /// Called when the view has been dismissed by gesture when presented modally (not in full screen). func threadsCoordinatorDidDismissInteractively(_ coordinator: ThreadsCoordinatorProtocol) } From 40b157e0ffdd81d1cf5037903eb6ef9cb1491615 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Mon, 22 Nov 2021 18:01:29 +0300 Subject: [PATCH 56/87] Fix title view for orientation --- .../Threads/ThreadList/ThreadListViewController.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Riot/Modules/Threads/ThreadList/ThreadListViewController.swift b/Riot/Modules/Threads/ThreadList/ThreadListViewController.swift index d027e6209..56f16c5df 100644 --- a/Riot/Modules/Threads/ThreadList/ThreadListViewController.swift +++ b/Riot/Modules/Threads/ThreadList/ThreadListViewController.swift @@ -89,7 +89,11 @@ final class ThreadListViewController: UIViewController { override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { guard let titleView = self.titleView else { return } - titleView.updateLayout(for: UIApplication.shared.statusBarOrientation) + if UIApplication.shared.statusBarOrientation.isPortrait { + titleView.updateLayout(for: .landscapeLeft) + } else { + titleView.updateLayout(for: .portrait) + } } // MARK: - Private @@ -122,6 +126,7 @@ final class ThreadListViewController: UIViewController { let titleView = ThreadRoomTitleView.loadFromNib() titleView.mode = .allThreads titleView.configure(withViewModel: viewModel.titleViewModel) + titleView.updateLayout(for: UIApplication.shared.statusBarOrientation) self.titleView = titleView navigationItem.leftItemsSupplementBackButton = true vc_removeBackTitle() From c3f3adcebfd37bea7cad05c41ffa77c132cefb5d Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Thu, 25 Nov 2021 01:33:37 +0300 Subject: [PATCH 57/87] Update deleted message design --- Riot/Assets/en.lproj/Vector.strings | 1 + Riot/Generated/Strings.swift | 4 ++++ Riot/Utils/EventFormatter.m | 26 ++++++++++++++++++++++++++ 3 files changed, 31 insertions(+) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 459c80331..388eed70e 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -917,6 +917,7 @@ Tap the + to start adding people."; "event_formatter_group_call_join" = "Join"; "event_formatter_group_call_leave" = "Leave"; "event_formatter_group_call_incoming" = "%@ in %@"; +"event_formatter_message_deleted" = "Message deleted"; // Events formatter with you "event_formatter_widget_added_by_you" = "You added the widget: %@"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 36a213c7d..148eb5b72 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -1359,6 +1359,10 @@ public class VectorL10n: NSObject { public static func eventFormatterMemberUpdates(_ p1: Int) -> String { return VectorL10n.tr("Vector", "event_formatter_member_updates", p1) } + /// Message deleted + public static var eventFormatterMessageDeleted: String { + return VectorL10n.tr("Vector", "event_formatter_message_deleted") + } /// (edited) public static var eventFormatterMessageEditedMention: String { return VectorL10n.tr("Vector", "event_formatter_message_edited_mention") diff --git a/Riot/Utils/EventFormatter.m b/Riot/Utils/EventFormatter.m index c7a11f1d0..368dba73f 100644 --- a/Riot/Utils/EventFormatter.m +++ b/Riot/Utils/EventFormatter.m @@ -65,6 +65,32 @@ static NSString *const kEventFormatterTimeFormat = @"HH:mm"; - (NSAttributedString *)attributedStringFromEvent:(MXEvent *)event withRoomState:(MXRoomState *)roomState error:(MXKEventFormatterError *)error { + if (event.redactedBecause) + { + // Check whether the event is a thread root or redacted information is required + if ([mxSession.threadingService isEventThreadRoot:event] + || self.settings.showRedactionsInRoomHistory) + { + UIFont *font = self.defaultTextFont; + UIColor *color = ThemeService.shared.theme.colors.secondaryContent; + NSString *string = [NSString stringWithFormat:@" %@", VectorL10n.eventFormatterMessageDeleted]; + NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:string + attributes:@{ + NSFontAttributeName: font, + NSForegroundColorAttributeName: color + }]; + + CGSize imageSize = CGSizeMake(20, 20); + NSTextAttachment *attachment = [[NSTextAttachment alloc] init]; + attachment.image = [[[UIImage imageNamed:@"room_context_menu_delete"] vc_resizedWith:imageSize] vc_tintedImageUsingColor:color]; + attachment.bounds = CGRectMake(0, font.descender, imageSize.width, imageSize.height); + NSAttributedString *imageString = [NSAttributedString attributedStringWithAttachment:attachment]; + + NSMutableAttributedString *result = [[NSMutableAttributedString alloc] initWithAttributedString:imageString]; + [result appendAttributedString:attrString]; + return result; + } + } BOOL isEventSenderMyUser = [event.sender isEqualToString:mxSession.myUserId]; // Build strings for widget events From 8f412d680671bc9caa6fcd940628c322b7428fca Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Thu, 25 Nov 2021 14:55:44 +0300 Subject: [PATCH 58/87] Implement actions for a thread --- Riot/Assets/en.lproj/Vector.strings | 1 + Riot/Generated/Strings.swift | 4 ++ Riot/Modules/Room/RoomViewController.m | 7 +- .../Threads/Thread/ThreadViewController.swift | 71 +++++++++++++++++++ 4 files changed, 77 insertions(+), 6 deletions(-) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 388eed70e..db1c4ab9d 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -419,6 +419,7 @@ Tap the + to start adding people."; // MARK: Threads "room_thread_title" = "Thread"; +"thread_copy_link_to_thread" = "Copy link to thread"; "threads_title" = "Threads"; "threads_action_all_threads" = "All threads"; "threads_action_my_threads" = "My threads"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 148eb5b72..a1db9cff3 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -5075,6 +5075,10 @@ public class VectorL10n: NSObject { public static var `switch`: String { return VectorL10n.tr("Vector", "switch") } + /// Copy link to thread + public static var threadCopyLinkToThread: String { + return VectorL10n.tr("Vector", "thread_copy_link_to_thread") + } /// All threads public static var threadsActionAllThreads: String { return VectorL10n.tr("Vector", "threads_action_all_threads") diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 7aebb0d2c..2ee271932 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -1520,7 +1520,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"room_context_menu_more"] style:UIBarButtonItemStylePlain target:self - action:@selector(onThreadMoreTapped:)]; + action:@selector(onButtonPressed:)]; item.accessibilityLabel = [VectorL10n roomAccessibilityThreadMore]; return item; @@ -4365,11 +4365,6 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [self placeCallWithVideo:YES]; } -- (IBAction)onThreadMoreTapped:(id)sender -{ - // TODO: Implement when design ready -} - - (IBAction)onThreadListTapped:(id)sender { self.threadsCoordinatorBridgePresenter = [[ThreadsCoordinatorBridgePresenter alloc] initWithSession:self.mainSession diff --git a/Riot/Modules/Threads/Thread/ThreadViewController.swift b/Riot/Modules/Threads/Thread/ThreadViewController.swift index 69625dd8a..879a6dd3a 100644 --- a/Riot/Modules/Threads/Thread/ThreadViewController.swift +++ b/Riot/Modules/Threads/Thread/ThreadViewController.swift @@ -22,6 +22,11 @@ class ThreadViewController: RoomViewController { private(set) var threadId: String! + private var permalink: String? { + guard let threadId = threadId else { return nil } + return MXTools.permalink(toEvent: threadId, inRoom: roomDataSource.roomId) + } + class func instantiate(withThreadId threadId: String, configuration: RoomDisplayConfiguration) -> ThreadViewController { let threadVC = ThreadViewController.instantiate(with: configuration) @@ -44,4 +49,70 @@ class ThreadViewController: RoomViewController { threadTitleView.mode = .specificThread(threadId: threadId) } + override func onButtonPressed(_ sender: Any) { + if let sender = sender as? UIBarButtonItem, sender == navigationItem.rightBarButtonItem { + showThreadActions() + return + } + super.onButtonPressed(sender) + } + + private func showThreadActions() { + let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) + + let viewInRoomAction = UIAlertAction(title: VectorL10n.roomEventActionViewInRoom, + style: .default, + handler: { [weak self] action in + guard let self = self else { return } + self.delegate?.roomViewController(self, + showRoomWithId: self.roomDataSource.roomId, + eventId: self.threadId) + }) + alertController.addAction(viewInRoomAction) + + let copyLinkAction = UIAlertAction(title: VectorL10n.threadCopyLinkToThread, + style: .default, + handler: { [weak self] action in + guard let self = self else { return } + self.copyPermalink() + }) + alertController.addAction(copyLinkAction) + + let shareAction = UIAlertAction(title: VectorL10n.roomEventActionShare, + style: .default, + handler: { [weak self] action in + guard let self = self else { return } + self.sharePermalink() + }) + alertController.addAction(shareAction) + + alertController.addAction(UIAlertAction(title: VectorL10n.cancel, + style: .cancel, + handler: nil)) + + alertController.popoverPresentationController?.barButtonItem = navigationItem.rightBarButtonItem + + self.present(alertController, animated: true, completion: nil) + } + + private func copyPermalink() { + guard let permalink = permalink else { + return + } + + MXKPasteboardManager.shared.pasteboard.string = permalink + } + + private func sharePermalink() { + guard let permalink = permalink else { + return + } + + let activityVC = UIActivityViewController(activityItems: [permalink], + applicationActivities: nil) + activityVC.modalTransitionStyle = .coverVertical + activityVC.popoverPresentationController?.barButtonItem = navigationItem.rightBarButtonItem + present(activityVC, animated: true, completion: nil) + } + } From 9839e6ae10e13fdac5f6328884a2093ae701c140 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Thu, 25 Nov 2021 16:28:42 +0300 Subject: [PATCH 59/87] Add threadId parameter to room navigation parameters --- Riot/Modules/Application/LegacyAppDelegate.m | 17 +++++++++++++++-- .../RoomNavigationParameters.swift | 5 +++++ .../RoomPreviewNavigationParameters.swift | 1 + .../Common/Recents/RecentsViewController.m | 1 + .../Files/HomeFilesSearchViewController.m | 1 + .../Messages/HomeMessagesSearchViewController.m | 1 + .../Rooms/DirectoryViewController.m | 1 + Riot/Modules/TabBar/TabBarCoordinator.swift | 3 ++- 8 files changed, 27 insertions(+), 3 deletions(-) diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index 668a41903..d664ca4c6 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -1280,6 +1280,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni } NSString *roomIdOrAlias; + NSString *threadId; NSString *eventId; NSString *userId; NSString *groupId; @@ -1361,7 +1362,16 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni else { // Open the room page - RoomNavigationParameters *roomNavigationParameters = [[RoomNavigationParameters alloc] initWithRoomId:roomId eventId:eventId mxSession:account.mxSession presentationParameters: screenPresentationParameters]; + if (eventId) + { + MXEvent *event = [account.mxSession.store eventWithEventId:eventId inRoom:roomId]; + threadId = event.threadIdentifier; + } + RoomNavigationParameters *roomNavigationParameters = [[RoomNavigationParameters alloc] initWithRoomId:roomId + threadId:threadId + eventId:eventId + mxSession:account.mxSession + presentationParameters:screenPresentationParameters]; [self showRoomWithParameters:roomNavigationParameters]; } @@ -2883,7 +2893,10 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni ScreenPresentationParameters *presentationParameters = [[ScreenPresentationParameters alloc] initWithRestoreInitialDisplay:YES]; RoomNavigationParameters *parameters = [[RoomNavigationParameters alloc] initWithRoomId:roomId - eventId:eventId mxSession:mxSession presentationParameters:presentationParameters]; + threadId:nil + eventId:eventId + mxSession:mxSession + presentationParameters:presentationParameters]; [self showRoomWithParameters:parameters]; } diff --git a/Riot/Modules/Application/ScreenNavigation/RoomNavigationParameters.swift b/Riot/Modules/Application/ScreenNavigation/RoomNavigationParameters.swift index 7308a5701..2c1880279 100644 --- a/Riot/Modules/Application/ScreenNavigation/RoomNavigationParameters.swift +++ b/Riot/Modules/Application/ScreenNavigation/RoomNavigationParameters.swift @@ -25,6 +25,9 @@ class RoomNavigationParameters: NSObject { /// The room identifier let roomId: String + /// If not nil, the thread will be opened on this room + let threadId: String? + /// If not nil, the room will be opened on this event. let eventId: String? @@ -37,10 +40,12 @@ class RoomNavigationParameters: NSObject { // MARK: - Setup init(roomId: String, + threadId: String?, eventId: String?, mxSession: MXSession, presentationParameters: ScreenPresentationParameters) { self.roomId = roomId + self.threadId = threadId self.eventId = eventId self.mxSession = mxSession self.presentationParameters = presentationParameters diff --git a/Riot/Modules/Application/ScreenNavigation/RoomPreviewNavigationParameters.swift b/Riot/Modules/Application/ScreenNavigation/RoomPreviewNavigationParameters.swift index e59d2687f..4a3137136 100644 --- a/Riot/Modules/Application/ScreenNavigation/RoomPreviewNavigationParameters.swift +++ b/Riot/Modules/Application/ScreenNavigation/RoomPreviewNavigationParameters.swift @@ -32,6 +32,7 @@ class RoomPreviewNavigationParameters: RoomNavigationParameters { self.previewData = previewData super.init(roomId: previewData.roomId, + threadId: nil, eventId: previewData.eventId, mxSession: previewData.mxSession, presentationParameters: presentationParameters) diff --git a/Riot/Modules/Common/Recents/RecentsViewController.m b/Riot/Modules/Common/Recents/RecentsViewController.m index c9b9bb3c0..f843d1c6c 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.m +++ b/Riot/Modules/Common/Recents/RecentsViewController.m @@ -880,6 +880,7 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro ScreenPresentationParameters *presentationParameters = [[ScreenPresentationParameters alloc] initWithRestoreInitialDisplay:NO stackAboveVisibleViews:NO]; RoomNavigationParameters *parameters = [[RoomNavigationParameters alloc] initWithRoomId:roomId + threadId:nil eventId:nil mxSession:matrixSession presentationParameters:presentationParameters]; diff --git a/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m b/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m index 63035f726..1fa201e3c 100644 --- a/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m +++ b/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m @@ -146,6 +146,7 @@ ScreenPresentationParameters *presentationParameters = [[ScreenPresentationParameters alloc] initWithRestoreInitialDisplay:NO stackAboveVisibleViews:NO]; RoomNavigationParameters *parameters = [[RoomNavigationParameters alloc] initWithRoomId:roomId + threadId:nil eventId:eventId mxSession:session presentationParameters:presentationParameters]; diff --git a/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m b/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m index 00fd9d20d..be56333a9 100644 --- a/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m +++ b/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m @@ -153,6 +153,7 @@ ScreenPresentationParameters *presentationParameters = [[ScreenPresentationParameters alloc] initWithRestoreInitialDisplay:NO stackAboveVisibleViews:NO]; RoomNavigationParameters *parameters = [[RoomNavigationParameters alloc] initWithRoomId:roomId + threadId:nil eventId:eventId mxSession:session presentationParameters:presentationParameters]; diff --git a/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m b/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m index 47300a956..cf364a6a4 100644 --- a/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m +++ b/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m @@ -237,6 +237,7 @@ ScreenPresentationParameters *presentationParameters = [[ScreenPresentationParameters alloc] initWithRestoreInitialDisplay:NO stackAboveVisibleViews:NO]; RoomNavigationParameters *parameters = [[RoomNavigationParameters alloc] initWithRoomId:roomId + threadId:nil eventId:nil mxSession:mxSession presentationParameters:presentationParameters]; diff --git a/Riot/Modules/TabBar/TabBarCoordinator.swift b/Riot/Modules/TabBar/TabBarCoordinator.swift index fafea903c..141fb5688 100644 --- a/Riot/Modules/TabBar/TabBarCoordinator.swift +++ b/Riot/Modules/TabBar/TabBarCoordinator.swift @@ -395,7 +395,8 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { let roomCoordinatorParameters = RoomCoordinatorParameters(navigationRouterStore: NavigationRouterStore.shared, session: roomNavigationParameters.mxSession, roomId: roomNavigationParameters.roomId, - eventId: roomNavigationParameters.eventId) + eventId: roomNavigationParameters.eventId, + threadId: roomNavigationParameters.threadId) self.showRoom(with: roomCoordinatorParameters, stackOnSplitViewDetail: roomNavigationParameters.presentationParameters.stackAboveVisibleViews, From 4b9a061ce112cc324520017a876626ae86c14061 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Fri, 26 Nov 2021 01:42:53 +0300 Subject: [PATCH 60/87] Adapt to event changes --- Riot/Modules/Room/DataSources/RoomDataSource.m | 8 ++++---- Riot/Modules/Room/RoomViewController.m | 6 +++--- Riot/Modules/Room/Search/RoomSearchViewController.m | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m index 19bc4fcf2..aa3313473 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.m +++ b/Riot/Modules/Room/DataSources/RoomDataSource.m @@ -249,7 +249,7 @@ const CGFloat kTypingCellHeight = 24; if (self.threadId) { // if in a thread, ignore non-root event or events from other threads - if (![event.eventId isEqualToString:self.threadId] && ![event.threadIdentifier isEqualToString:self.threadId]) + if (![event.eventId isEqualToString:self.threadId] && ![event.threadId isEqualToString:self.threadId]) { // Ignore the event return NO; @@ -259,7 +259,7 @@ const CGFloat kTypingCellHeight = 24; { MXEvent *relatedEvent = [self.mxSession.store eventWithEventId:event.relatesTo.eventId inRoom:event.roomId]; - if (![relatedEvent.threadIdentifier isEqualToString:self.threadId]) + if (![relatedEvent.threadId isEqualToString:self.threadId]) { // ignore the event return NO; @@ -269,7 +269,7 @@ const CGFloat kTypingCellHeight = 24; else { // if not in a thread, ignore all threaded events - if (event.threadIdentifier) + if (event.threadId) { // ignore the event return NO; @@ -279,7 +279,7 @@ const CGFloat kTypingCellHeight = 24; { MXEvent *relatedEvent = [self.mxSession.store eventWithEventId:event.relatesTo.eventId inRoom:event.roomId]; - if (relatedEvent.threadIdentifier) + if (relatedEvent.threadId) { // ignore the event return NO; diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index d8925cbab..a27e769f2 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -3211,7 +3211,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; BOOL showThreadOption = RiotSettings.shared.enableThreads && !self.roomDataSource.threadId - && !selectedEvent.threadIdentifier; + && !selectedEvent.threadId; if (showThreadOption && [self canCopyEvent:selectedEvent andCell:cell]) { [currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionCopy] @@ -4022,7 +4022,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; - (void)roomDataSource:(RoomDataSource *)roomDataSource didTapThread:(MXThread *)thread { - [self openThreadWithId:thread.identifier]; + [self openThreadWithId:thread.id]; } #pragma mark - Segues @@ -5937,7 +5937,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; BOOL showMoreOption = (event.isState && RiotSettings.shared.roomContextualMenuShowMoreOptionForStates) || (!event.isState && RiotSettings.shared.roomContextualMenuShowMoreOptionForMessages); - BOOL showThreadOption = RiotSettings.shared.enableThreads && !self.roomDataSource.threadId && !event.threadIdentifier; + BOOL showThreadOption = RiotSettings.shared.enableThreads && !self.roomDataSource.threadId && !event.threadId; NSMutableArray *items = [NSMutableArray arrayWithCapacity:5]; diff --git a/Riot/Modules/Room/Search/RoomSearchViewController.m b/Riot/Modules/Room/Search/RoomSearchViewController.m index 850fcd913..42315cf1e 100644 --- a/Riot/Modules/Room/Search/RoomSearchViewController.m +++ b/Riot/Modules/Room/Search/RoomSearchViewController.m @@ -317,7 +317,7 @@ [RoomDataSource loadRoomDataSourceWithRoomId:selectedSearchEvent.roomId initialEventId:selectedSearchEvent.eventId - threadId:selectedSearchEvent.threadIdentifier + threadId:selectedSearchEvent.threadId andMatrixSession:selectedSearchEventSession onComplete:^(RoomDataSource *roomDataSource) { From 24838f99ac7791dc26024cb558b29cbffea2b108 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Fri, 26 Nov 2021 01:47:08 +0300 Subject: [PATCH 61/87] Adapt to event changes --- Riot/Modules/Application/LegacyAppDelegate.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index d664ca4c6..1025533c4 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -1365,7 +1365,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni if (eventId) { MXEvent *event = [account.mxSession.store eventWithEventId:eventId inRoom:roomId]; - threadId = event.threadIdentifier; + threadId = event.threadId; } RoomNavigationParameters *roomNavigationParameters = [[RoomNavigationParameters alloc] initWithRoomId:roomId threadId:threadId From 47a4ba641a72c67a0a4bfba0019a2ce42b091617 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Mon, 29 Nov 2021 13:55:13 +0300 Subject: [PATCH 62/87] Adapt to thread model changes --- Riot/Modules/Threads/ThreadsCoordinator.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Modules/Threads/ThreadsCoordinator.swift b/Riot/Modules/Threads/ThreadsCoordinator.swift index 6b2cee444..807d6d125 100644 --- a/Riot/Modules/Threads/ThreadsCoordinator.swift +++ b/Riot/Modules/Threads/ThreadsCoordinator.swift @@ -122,7 +122,7 @@ final class ThreadsCoordinator: NSObject, ThreadsCoordinatorProtocol { session: parameters.session, roomId: parameters.roomId, eventId: nil, - threadId: thread.identifier, + threadId: thread.id, displayConfiguration: .forThreads) let coordinator = RoomCoordinator(parameters: parameters) coordinator.delegate = self From 3c80fb0fc97fd3064fd3c5fbe4cf7a3055df818e Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Mon, 29 Nov 2021 18:09:38 +0300 Subject: [PATCH 63/87] Refactor navigation router with `PresentableModule` --- Riot/Routers/NavigationRouter.swift | 42 +++++++++++++++++++++---- Riot/Routers/NavigationRouterType.swift | 40 +++++++++++++++++++++-- Riot/Routers/Presentable.swift | 10 ++++++ Riot/Routers/PresentableModule.swift | 36 +++++++++++++++++++++ 4 files changed, 120 insertions(+), 8 deletions(-) create mode 100644 Riot/Routers/PresentableModule.swift diff --git a/Riot/Routers/NavigationRouter.swift b/Riot/Routers/NavigationRouter.swift index 25a0aa333..c406cc7e7 100755 --- a/Riot/Routers/NavigationRouter.swift +++ b/Riot/Routers/NavigationRouter.swift @@ -117,13 +117,13 @@ final class NavigationRouter: NSObject, NavigationRouterType { self.didPushViewController(controller) } - func setModules(_ modules: [Presentable], hideNavigationBar: Bool, animated: Bool) { + func setModules(_ modules: [PresentableModule], hideNavigationBar: Bool, animated: Bool) { MXLog.debug("[NavigationRouter] Set modules \(modules)") - let controllers = modules.map { (presentable) -> UIViewController in - let controller = presentable.toPresentable() - self.addModule(presentable, for: controller) + let controllers = modules.map { (module) -> UIViewController in + let controller = module.presentable.toPresentable() + self.addModule(module.presentable, for: controller) return controller } @@ -147,8 +147,8 @@ final class NavigationRouter: NSObject, NavigationRouterType { } // Add again controller to module association, in case same modules instance are added back - modules.forEach { (presentable) in - self.addModule(presentable, for: presentable.toPresentable()) + modules.forEach { (module) in + self.addModule(module.presentable, for: module.presentable.toPresentable()) } controllers.forEach { @@ -221,6 +221,36 @@ final class NavigationRouter: NSObject, NavigationRouterType { self.didPushViewController(controller) } + func push(_ modules: [PresentableModule], animated: Bool) { + MXLog.debug("[NavigationRouter] Push modules \(modules)") + + // Avoid pushing any UINavigationController onto stack + guard modules.first(where: { $0.presentable.toPresentable() is UINavigationController }) == nil else { + MXLog.error("Cannot push a UINavigationController to NavigationRouter") + return + } + + for module in modules { + let controller = module.presentable.toPresentable() + self.addModule(module.presentable, for: controller) + + if let completion = module.popCompletion { + completions[controller] = completion + } + + self.willPushViewController(controller) + } + + var viewControllers = navigationController.viewControllers + viewControllers.append(contentsOf: modules.map({ $0.presentable.toPresentable() })) + navigationController.setViewControllers(viewControllers, animated: animated) + + for module in modules { + let controller = module.presentable.toPresentable() + self.didPushViewController(controller) + } + } + func popModule(animated: Bool = true) { MXLog.debug("[NavigationRouter] Pop module") diff --git a/Riot/Routers/NavigationRouterType.swift b/Riot/Routers/NavigationRouterType.swift index c7c9aa2ec..ca248526f 100755 --- a/Riot/Routers/NavigationRouterType.swift +++ b/Riot/Routers/NavigationRouterType.swift @@ -42,10 +42,10 @@ protocol NavigationRouterType: AnyObject, Presentable { /// Set view controllers stack of navigation controller /// - Parameters: - /// - modules: The presentables stack to set. + /// - modules: The modules stack to set. /// - hideNavigationBar: Specify true to hide the UINavigationBar. /// - animated: Specify true to animate the transition. - func setModules(_ modules: [Presentable], hideNavigationBar: Bool, animated: Bool) + func setModules(_ modules: [PresentableModule], hideNavigationBar: Bool, animated: Bool) /// Pop to root view controller of navigation controller and remove all others /// @@ -64,6 +64,12 @@ protocol NavigationRouterType: AnyObject, Presentable { /// - Parameter popCompletion: Completion called when `module` is removed from the navigation stack. func push(_ module: Presentable, animated: Bool, popCompletion: (() -> Void)?) + /// Push some view controllers on navigation controller stack + /// + /// - Parameter modules: Modules to push + /// - Parameter animated: Specify true to animate the transition. + func push(_ modules: [PresentableModule], animated: Bool) + /// Pop last view controller from navigation controller stack /// /// - Parameter animated: Specify true to animate the transition. @@ -93,7 +99,37 @@ extension NavigationRouterType { setRootModule(module, hideNavigationBar: false, animated: false, popCompletion: popCompletion) } + func setModules(_ modules: [PresentableModule], animated: Bool) { + setModules(modules, hideNavigationBar: false, animated: animated) + } + func setModules(_ modules: [Presentable], animated: Bool) { setModules(modules, hideNavigationBar: false, animated: animated) } + +} + +// MARK: - Presentable <--> ModulePresentable Transitive Methods + +extension NavigationRouterType { + + func setRootModule(_ module: PresentableModule) { + setRootModule(module.presentable, popCompletion: module.popCompletion) + } + + func push(_ module: PresentableModule, animated: Bool) { + push(module.presentable, animated: animated, popCompletion: module.popCompletion) + } + + func setModules(_ modules: [Presentable], hideNavigationBar: Bool, animated: Bool) { + setModules(modules.map { $0.toModule() }, + hideNavigationBar: hideNavigationBar, + animated: animated) + } + + func push(_ modules: [Presentable], animated: Bool) { + push(modules.map { $0.toModule() }, + animated: animated) + } + } diff --git a/Riot/Routers/Presentable.swift b/Riot/Routers/Presentable.swift index fd42d3059..15943afbf 100755 --- a/Riot/Routers/Presentable.swift +++ b/Riot/Routers/Presentable.swift @@ -26,3 +26,13 @@ extension UIViewController: Presentable { return self } } + +extension Presentable { + + /// Returns a new module from the presentable without a pop completion block + /// - Returns: Module + func toModule() -> PresentableModule { + return PresentableModule(presentable: self, popCompletion: nil) + } + +} diff --git a/Riot/Routers/PresentableModule.swift b/Riot/Routers/PresentableModule.swift new file mode 100644 index 000000000..4ee7e3379 --- /dev/null +++ b/Riot/Routers/PresentableModule.swift @@ -0,0 +1,36 @@ +// +// 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 Foundation + +/// Structure used to pass modules to routers with pop completion blocks. +struct PresentableModule { + /// Actual presentable of the module + let presentable: Presentable + + /// Block to be called when the module is popped + let popCompletion: (() -> Void)? +} + +// MARK: - CustomStringConvertible + +extension PresentableModule: CustomStringConvertible { + + var description: String { + return "PresentableModule: \(presentable), pop completion: \(String(describing: popCompletion))" + } + +} From 6209b0f9b83edf5c3493df93a4a7014db800b492 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Mon, 29 Nov 2021 18:11:04 +0300 Subject: [PATCH 64/87] Replace threadId with `ThreadParameters` --- .../RoomNavigationParameters.swift | 28 +++++++++++++++---- .../RoomPreviewNavigationParameters.swift | 2 +- .../Common/Recents/RecentsViewController.m | 2 +- .../Files/HomeFilesSearchViewController.m | 2 +- .../HomeMessagesSearchViewController.m | 2 +- .../Rooms/DirectoryViewController.m | 2 +- 6 files changed, 28 insertions(+), 10 deletions(-) diff --git a/Riot/Modules/Application/ScreenNavigation/RoomNavigationParameters.swift b/Riot/Modules/Application/ScreenNavigation/RoomNavigationParameters.swift index 2c1880279..b44d00356 100644 --- a/Riot/Modules/Application/ScreenNavigation/RoomNavigationParameters.swift +++ b/Riot/Modules/Application/ScreenNavigation/RoomNavigationParameters.swift @@ -16,6 +16,24 @@ import Foundation +@objcMembers +class ThreadParameters: NSObject { + + /// If not nil, the thread will be opened on this room + let threadId: String + + /// If true, related room screen will be stacked in the navigation stack + let stackRoomScreen: Bool + + init(threadId: String, + stackRoomScreen: Bool) { + self.threadId = threadId + self.stackRoomScreen = stackRoomScreen + super.init() + } + +} + /// Navigation parameters to display a room with a provided identifier in a specific matrix session. @objcMembers class RoomNavigationParameters: NSObject { @@ -25,29 +43,29 @@ class RoomNavigationParameters: NSObject { /// The room identifier let roomId: String - /// If not nil, the thread will be opened on this room - let threadId: String? - /// If not nil, the room will be opened on this event. let eventId: String? /// The Matrix session in which the room should be available. let mxSession: MXSession + /// Navigation parameters for a thread + let threadParameters: ThreadParameters? + /// Screen presentation parameters. let presentationParameters: ScreenPresentationParameters // MARK: - Setup init(roomId: String, - threadId: String?, eventId: String?, mxSession: MXSession, + threadParameters: ThreadParameters?, presentationParameters: ScreenPresentationParameters) { self.roomId = roomId - self.threadId = threadId self.eventId = eventId self.mxSession = mxSession + self.threadParameters = threadParameters self.presentationParameters = presentationParameters super.init() diff --git a/Riot/Modules/Application/ScreenNavigation/RoomPreviewNavigationParameters.swift b/Riot/Modules/Application/ScreenNavigation/RoomPreviewNavigationParameters.swift index 4a3137136..3a9532c02 100644 --- a/Riot/Modules/Application/ScreenNavigation/RoomPreviewNavigationParameters.swift +++ b/Riot/Modules/Application/ScreenNavigation/RoomPreviewNavigationParameters.swift @@ -32,9 +32,9 @@ class RoomPreviewNavigationParameters: RoomNavigationParameters { self.previewData = previewData super.init(roomId: previewData.roomId, - threadId: nil, eventId: previewData.eventId, mxSession: previewData.mxSession, + threadParameters: nil, presentationParameters: presentationParameters) } } diff --git a/Riot/Modules/Common/Recents/RecentsViewController.m b/Riot/Modules/Common/Recents/RecentsViewController.m index f843d1c6c..1b5fdd385 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.m +++ b/Riot/Modules/Common/Recents/RecentsViewController.m @@ -880,9 +880,9 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro ScreenPresentationParameters *presentationParameters = [[ScreenPresentationParameters alloc] initWithRestoreInitialDisplay:NO stackAboveVisibleViews:NO]; RoomNavigationParameters *parameters = [[RoomNavigationParameters alloc] initWithRoomId:roomId - threadId:nil eventId:nil mxSession:matrixSession + threadParameters:nil presentationParameters:presentationParameters]; [[AppDelegate theDelegate] showRoomWithParameters:parameters completion:^{ diff --git a/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m b/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m index 1fa201e3c..8a77c3200 100644 --- a/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m +++ b/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m @@ -146,9 +146,9 @@ ScreenPresentationParameters *presentationParameters = [[ScreenPresentationParameters alloc] initWithRestoreInitialDisplay:NO stackAboveVisibleViews:NO]; RoomNavigationParameters *parameters = [[RoomNavigationParameters alloc] initWithRoomId:roomId - threadId:nil eventId:eventId mxSession:session + threadParameters:nil presentationParameters:presentationParameters]; [[AppDelegate theDelegate] showRoomWithParameters:parameters]; diff --git a/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m b/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m index be56333a9..e001228b7 100644 --- a/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m +++ b/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m @@ -153,9 +153,9 @@ ScreenPresentationParameters *presentationParameters = [[ScreenPresentationParameters alloc] initWithRestoreInitialDisplay:NO stackAboveVisibleViews:NO]; RoomNavigationParameters *parameters = [[RoomNavigationParameters alloc] initWithRoomId:roomId - threadId:nil eventId:eventId mxSession:session + threadParameters:nil presentationParameters:presentationParameters]; [[AppDelegate theDelegate] showRoomWithParameters:parameters]; diff --git a/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m b/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m index cf364a6a4..e01bd3d2b 100644 --- a/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m +++ b/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m @@ -237,9 +237,9 @@ ScreenPresentationParameters *presentationParameters = [[ScreenPresentationParameters alloc] initWithRestoreInitialDisplay:NO stackAboveVisibleViews:NO]; RoomNavigationParameters *parameters = [[RoomNavigationParameters alloc] initWithRoomId:roomId - threadId:nil eventId:nil mxSession:mxSession + threadParameters:nil presentationParameters:presentationParameters]; [[AppDelegate theDelegate] showRoomWithParameters:parameters]; } From 2b759efeb7652291573df9fbb80645aad3c218b3 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Mon, 29 Nov 2021 18:11:54 +0300 Subject: [PATCH 65/87] Introduce new methods to split view presentable --- .../SplitView/SplitViewCoordinator.swift | 24 +++++++++++++++++++ .../SplitView/SplitViewPresentable.swift | 9 +++++++ 2 files changed, 33 insertions(+) diff --git a/Riot/Modules/SplitView/SplitViewCoordinator.swift b/Riot/Modules/SplitView/SplitViewCoordinator.swift index ee1ad5af7..bd56a095a 100644 --- a/Riot/Modules/SplitView/SplitViewCoordinator.swift +++ b/Riot/Modules/SplitView/SplitViewCoordinator.swift @@ -363,6 +363,30 @@ extension SplitViewCoordinator: SplitViewMasterPresentableDelegate { detailNavigationRouter.push(detailPresentable, animated: true, popCompletion: popCompletion) } + func splitViewMasterPresentable(_ presentable: Presentable, wantsToReplaceDetailsWith modules: [PresentableModule]) { + MXLog.debug("[SplitViewCoordinator] splitViewMasterPresentable: \(presentable) wantsToReplaceDetailsWith modules: \(modules)") + + self.detailNavigationRouter?.setModules(modules, animated: true) + } + + func splitViewMasterPresentable(_ presentable: Presentable, wantsToStack modules: [PresentableModule]) { + guard let detailNavigationRouter = self.detailNavigationRouter else { + MXLog.warning("[SplitViewCoordinator] Failed to stack \(modules) because detailNavigationRouter is nil") + return + } + + detailNavigationRouter.push(modules, animated: true) + } + + func splitViewMasterPresentable(_ presentable: Presentable, wantsToPopTo module: Presentable) { + guard let detailNavigationRouter = self.detailNavigationRouter else { + MXLog.warning("[SplitViewCoordinator] Failed to pop to \(module) because detailNavigationRouter is nil") + return + } + + detailNavigationRouter.popToModule(module, animated: true) + } + func splitViewMasterPresentableWantsToResetDetail(_ presentable: Presentable) { self.resetDetails(animated: false) } diff --git a/Riot/Modules/SplitView/SplitViewPresentable.swift b/Riot/Modules/SplitView/SplitViewPresentable.swift index 7932a4815..7dcb0da12 100644 --- a/Riot/Modules/SplitView/SplitViewPresentable.swift +++ b/Riot/Modules/SplitView/SplitViewPresentable.swift @@ -27,6 +27,15 @@ protocol SplitViewMasterPresentableDelegate: AnyObject { /// Stack the detailPresentable on the existing split view detail stack func splitViewMasterPresentable(_ presentable: Presentable, wantsToStack detailPresentable: Presentable, popCompletion: (() -> Void)?) + /// Replace split view detail with the given modules + func splitViewMasterPresentable(_ presentable: Presentable, wantsToReplaceDetailsWith modules: [PresentableModule]) + + /// Stack modules on the existing split view detail stack + func splitViewMasterPresentable(_ presentable: Presentable, wantsToStack modules: [PresentableModule]) + + /// Pop to module on the existing split view detail stack + func splitViewMasterPresentable(_ presentable: Presentable, wantsToPopTo module: Presentable) + /// Reset detail stack with placeholder func splitViewMasterPresentableWantsToResetDetail(_ presentable: Presentable) } From 7594be70a946f42d579902a0a6d9b1a22840f01a Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Mon, 29 Nov 2021 18:12:20 +0300 Subject: [PATCH 66/87] Add threadId to room identification --- Riot/Modules/Room/RoomCoordinator.swift | 4 ++++ Riot/Modules/Room/RoomIdentifiable.swift | 1 + 2 files changed, 5 insertions(+) diff --git a/Riot/Modules/Room/RoomCoordinator.swift b/Riot/Modules/Room/RoomCoordinator.swift index 1def16ba7..0b1ff3581 100644 --- a/Riot/Modules/Room/RoomCoordinator.swift +++ b/Riot/Modules/Room/RoomCoordinator.swift @@ -243,6 +243,10 @@ extension RoomCoordinator: RoomIdentifiable { return self.parameters.roomId } + var threadId: String? { + return self.parameters.threadId + } + var mxSession: MXSession? { self.parameters.session } diff --git a/Riot/Modules/Room/RoomIdentifiable.swift b/Riot/Modules/Room/RoomIdentifiable.swift index 2ccac42d7..b86770360 100644 --- a/Riot/Modules/Room/RoomIdentifiable.swift +++ b/Riot/Modules/Room/RoomIdentifiable.swift @@ -20,5 +20,6 @@ import Foundation /// Useful to identify existing objects that should be removed when the user leaves a room for example. protocol RoomIdentifiable { var roomId: String? { get } + var threadId: String? { get } var mxSession: MXSession? { get } } From 5c177e2aec81f3765add9c1f1cf464e6cb38b42f Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Mon, 29 Nov 2021 18:22:09 +0300 Subject: [PATCH 67/87] Pass thread options for permalinks --- Riot/Modules/Application/LegacyAppDelegate.m | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index 1025533c4..8641680c2 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -1280,7 +1280,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni } NSString *roomIdOrAlias; - NSString *threadId; + ThreadParameters *threadParameters; NSString *eventId; NSString *userId; NSString *groupId; @@ -1365,12 +1365,16 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni if (eventId) { MXEvent *event = [account.mxSession.store eventWithEventId:eventId inRoom:roomId]; - threadId = event.threadId; + if (event.threadId) + { + threadParameters = [[ThreadParameters alloc] initWithThreadId:event.threadId + stackRoomScreen:YES]; + } } RoomNavigationParameters *roomNavigationParameters = [[RoomNavigationParameters alloc] initWithRoomId:roomId - threadId:threadId eventId:eventId mxSession:account.mxSession + threadParameters:threadParameters presentationParameters:screenPresentationParameters]; [self showRoomWithParameters:roomNavigationParameters]; @@ -2893,9 +2897,9 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni ScreenPresentationParameters *presentationParameters = [[ScreenPresentationParameters alloc] initWithRestoreInitialDisplay:YES]; RoomNavigationParameters *parameters = [[RoomNavigationParameters alloc] initWithRoomId:roomId - threadId:nil eventId:eventId mxSession:mxSession + threadParameters:nil presentationParameters:presentationParameters]; [self showRoomWithParameters:parameters]; From 00ec31494febd356d21ed3ddc6cbfcc0ad5e85c7 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Mon, 29 Nov 2021 18:26:10 +0300 Subject: [PATCH 68/87] Try to find the desired room in whole stack instead of only the last, and pop to it if found --- Riot/Modules/TabBar/TabBarCoordinator.swift | 28 ++++++++++++--------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/Riot/Modules/TabBar/TabBarCoordinator.swift b/Riot/Modules/TabBar/TabBarCoordinator.swift index 141fb5688..79c4625cc 100644 --- a/Riot/Modules/TabBar/TabBarCoordinator.swift +++ b/Riot/Modules/TabBar/TabBarCoordinator.swift @@ -437,18 +437,22 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { stackOnSplitViewDetail: Bool = false, completion: (() -> Void)? = nil) { - if let topRoomCoordinator = self.splitViewMasterPresentableDelegate?.detailModules.last as? RoomCoordinatorProtocol, - parameters.roomId == topRoomCoordinator.roomId && parameters.session == topRoomCoordinator.mxSession { - - // RoomCoordinator with the same room id and Matrix session is shown - - if let eventId = parameters.eventId { - // If there is an event id ask the RoomCoordinator to start with this one - topRoomCoordinator.start(withEventId: eventId, completion: completion) - } else { - // If there is no event id defined do nothing - completion?() - } + // try to find the desired room screen in the stack + if let roomCoordinator = self.splitViewMasterPresentableDelegate?.detailModules.last(where: { presentable in + guard let roomCoordinator = presentable as? RoomCoordinatorProtocol else { + return false + } + return roomCoordinator.roomId == parameters.roomId + && roomCoordinator.threadId == parameters.threadId + && roomCoordinator.mxSession == parameters.session + }) as? RoomCoordinatorProtocol { + self.splitViewMasterPresentableDelegate?.splitViewMasterPresentable(self, wantsToPopTo: roomCoordinator) + // go to a specific event if provided + if let eventId = parameters.eventId { + roomCoordinator.start(withEventId: eventId, completion: completion) + } else { + completion?() + } return } From 113383576bab08c950f5d526acebfaa7c16bdd81 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Mon, 29 Nov 2021 18:27:00 +0300 Subject: [PATCH 69/87] Handle thread parameters --- Riot/Modules/TabBar/TabBarCoordinator.swift | 76 ++++++++++++++++++--- 1 file changed, 67 insertions(+), 9 deletions(-) diff --git a/Riot/Modules/TabBar/TabBarCoordinator.swift b/Riot/Modules/TabBar/TabBarCoordinator.swift index 79c4625cc..9141cce4e 100644 --- a/Riot/Modules/TabBar/TabBarCoordinator.swift +++ b/Riot/Modules/TabBar/TabBarCoordinator.swift @@ -392,15 +392,65 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { private func showRoom(withNavigationParameters roomNavigationParameters: RoomNavigationParameters, completion: (() -> Void)?) { - let roomCoordinatorParameters = RoomCoordinatorParameters(navigationRouterStore: NavigationRouterStore.shared, - session: roomNavigationParameters.mxSession, - roomId: roomNavigationParameters.roomId, - eventId: roomNavigationParameters.eventId, - threadId: roomNavigationParameters.threadId) - - self.showRoom(with: roomCoordinatorParameters, - stackOnSplitViewDetail: roomNavigationParameters.presentationParameters.stackAboveVisibleViews, - completion: completion) + if let threadParameters = roomNavigationParameters.threadParameters, threadParameters.stackRoomScreen { + let dispatchGroup = DispatchGroup() + + // create room coordinator + let roomCoordinatorParameters = RoomCoordinatorParameters(navigationRouterStore: NavigationRouterStore.shared, + session: roomNavigationParameters.mxSession, + roomId: roomNavigationParameters.roomId, + eventId: nil, + threadId: nil) + + dispatchGroup.enter() + let roomCoordinator = RoomCoordinator(parameters: roomCoordinatorParameters) + roomCoordinator.delegate = self + roomCoordinator.start { + dispatchGroup.leave() + } + self.add(childCoordinator: roomCoordinator) + + // create thread coordinator + let threadCoordinatorParameters = RoomCoordinatorParameters(navigationRouterStore: NavigationRouterStore.shared, + session: roomNavigationParameters.mxSession, + roomId: roomNavigationParameters.roomId, + eventId: roomNavigationParameters.eventId, + threadId: roomNavigationParameters.threadParameters?.threadId) + + dispatchGroup.enter() + let threadCoordinator = RoomCoordinator(parameters: threadCoordinatorParameters) + threadCoordinator.delegate = self + threadCoordinator.start { + dispatchGroup.leave() + } + self.add(childCoordinator: threadCoordinator) + + dispatchGroup.notify(queue: .main) { + let modules: [PresentableModule] = [ + PresentableModule(presentable: roomCoordinator, popCompletion: { [weak self] in + // NOTE: The RoomDataSource releasing is handled in SplitViewCoordinator + self?.remove(childCoordinator: roomCoordinator) + }), + PresentableModule(presentable: threadCoordinator, popCompletion: { [weak self] in + // NOTE: The RoomDataSource releasing is handled in SplitViewCoordinator + self?.remove(childCoordinator: threadCoordinator) + }) + ] + + self.showSplitViewDetails(with: modules, + stack: roomNavigationParameters.presentationParameters.stackAboveVisibleViews) + } + } else { + let roomCoordinatorParameters = RoomCoordinatorParameters(navigationRouterStore: NavigationRouterStore.shared, + session: roomNavigationParameters.mxSession, + roomId: roomNavigationParameters.roomId, + eventId: roomNavigationParameters.eventId, + threadId: roomNavigationParameters.threadParameters?.threadId) + + self.showRoom(with: roomCoordinatorParameters, + stackOnSplitViewDetail: roomNavigationParameters.presentationParameters.stackAboveVisibleViews, + completion: completion) + } } private func showRoom(with roomId: String, eventId: String?, matrixSession: MXSession, completion: (() -> Void)? = nil) { @@ -488,6 +538,14 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { } } + private func showSplitViewDetails(with modules: [PresentableModule], stack: Bool) { + if stack { + self.splitViewMasterPresentableDelegate?.splitViewMasterPresentable(self, wantsToStack: modules) + } else { + self.splitViewMasterPresentableDelegate?.splitViewMasterPresentable(self, wantsToReplaceDetailsWith: modules) + } + } + private func resetSplitViewDetails() { self.splitViewMasterPresentableDelegate?.splitViewMasterPresentableWantsToResetDetail(self) } From 434585417f837bf38b40b1a26e743498470252ad Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Mon, 29 Nov 2021 18:27:57 +0300 Subject: [PATCH 70/87] Add changelog --- changelog.d/5094.change | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/5094.change diff --git a/changelog.d/5094.change b/changelog.d/5094.change new file mode 100644 index 000000000..4995110eb --- /dev/null +++ b/changelog.d/5094.change @@ -0,0 +1 @@ +Permalinks: Create for thread events & handle navigations. From ffc0c4ee419592efd974f7d2de9a8edf69f8b977 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Tue, 30 Nov 2021 00:31:28 +0300 Subject: [PATCH 71/87] Highlight initial event in the thread view --- Riot/Modules/Room/RoomCoordinator.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Riot/Modules/Room/RoomCoordinator.swift b/Riot/Modules/Room/RoomCoordinator.swift index 0b1ff3581..c7e24220a 100644 --- a/Riot/Modules/Room/RoomCoordinator.swift +++ b/Riot/Modules/Room/RoomCoordinator.swift @@ -107,7 +107,10 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { if let previewData = self.parameters.previewData { self.loadRoomPreview(withData: previewData, completion: completion) } else if let threadId = self.parameters.threadId { - self.loadRoom(withId: self.parameters.roomId, andThreadId: threadId, completion: completion) + self.loadRoom(withId: self.parameters.roomId, + andThreadId: threadId, + eventId: self.parameters.eventId, + completion: completion) } else if let eventId = self.selectedEventId { self.loadRoom(withId: self.parameters.roomId, andEventId: eventId, completion: completion) } else { @@ -196,7 +199,7 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { } } - private func loadRoom(withId roomId: String, andThreadId threadId: String, completion: (() -> Void)?) { + private func loadRoom(withId roomId: String, andThreadId threadId: String, eventId: String?, completion: (() -> Void)?) { // Present activity indicator when retrieving roomDataSource for given room ID self.activityIndicatorPresenter.presentActivityIndicator(on: roomViewController.view, animated: false) @@ -218,6 +221,7 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { } threadDataSource.markTimelineInitialEvent = false + threadDataSource.highlightedEventId = eventId self.roomViewController.displayRoom(threadDataSource) // Give the data source ownership to the room view controller. From fe336e05244659e95a93d97f9306644ca4da610d Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Tue, 30 Nov 2021 00:59:59 +0300 Subject: [PATCH 72/87] Add activity indicator when loading room and thread data sources --- Riot/Modules/TabBar/TabBarCoordinator.swift | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Riot/Modules/TabBar/TabBarCoordinator.swift b/Riot/Modules/TabBar/TabBarCoordinator.swift index 9141cce4e..05f07a77e 100644 --- a/Riot/Modules/TabBar/TabBarCoordinator.swift +++ b/Riot/Modules/TabBar/TabBarCoordinator.swift @@ -26,6 +26,7 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { // MARK: Private private let parameters: TabBarCoordinatorParameters + private let activityIndicatorPresenter: ActivityIndicatorPresenterType // Indicate if the Coordinator has started once private var hasStartedOnce: Bool { @@ -69,6 +70,7 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { let masterNavigationController = RiotNavigationController() self.navigationRouter = NavigationRouter(navigationController: masterNavigationController) self.masterNavigationController = masterNavigationController + self.activityIndicatorPresenter = ActivityIndicatorPresenter() } // MARK: - Public methods @@ -393,6 +395,7 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { private func showRoom(withNavigationParameters roomNavigationParameters: RoomNavigationParameters, completion: (() -> Void)?) { if let threadParameters = roomNavigationParameters.threadParameters, threadParameters.stackRoomScreen { + self.activityIndicatorPresenter.presentActivityIndicator(on: toPresentable().view, animated: false) let dispatchGroup = DispatchGroup() // create room coordinator @@ -425,7 +428,8 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { } self.add(childCoordinator: threadCoordinator) - dispatchGroup.notify(queue: .main) { + dispatchGroup.notify(queue: .main) { [weak self] in + guard let self = self else { return } let modules: [PresentableModule] = [ PresentableModule(presentable: roomCoordinator, popCompletion: { [weak self] in // NOTE: The RoomDataSource releasing is handled in SplitViewCoordinator @@ -439,6 +443,8 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { self.showSplitViewDetails(with: modules, stack: roomNavigationParameters.presentationParameters.stackAboveVisibleViews) + + self.activityIndicatorPresenter.removeCurrentActivityIndicator(animated: true) } } else { let roomCoordinatorParameters = RoomCoordinatorParameters(navigationRouterStore: NavigationRouterStore.shared, From de7a85e83bc902a9b4420ad011401c91005fe581 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Tue, 30 Nov 2021 01:12:53 +0300 Subject: [PATCH 73/87] Open thread view for root events when available --- Riot/Modules/Application/LegacyAppDelegate.m | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index 8641680c2..09a705e05 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -1370,6 +1370,11 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni threadParameters = [[ThreadParameters alloc] initWithThreadId:event.threadId stackRoomScreen:YES]; } + else if ([account.mxSession.threadingService isEventThreadRoot:event]) + { + threadParameters = [[ThreadParameters alloc] initWithThreadId:event.eventId + stackRoomScreen:YES]; + } } RoomNavigationParameters *roomNavigationParameters = [[RoomNavigationParameters alloc] initWithRoomId:roomId eventId:eventId From 819489c9d338a150b82a246bb923923b9679afb2 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Tue, 30 Nov 2021 01:13:14 +0300 Subject: [PATCH 74/87] Display room title view for non-live timelines too --- Riot/Modules/Room/RoomViewController.m | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 4ee26b79f..ad24b8fdf 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -1752,12 +1752,12 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; } [rightBarButtonItems addObject:item]; } - - // Do not change title view class here if the expanded header is visible. - [self setRoomTitleViewClass:RoomTitleView.class]; - ((RoomTitleView*)self.titleView).tapGestureDelegate = self; } + // Do not change title view class here if the expanded header is visible. + [self setRoomTitleViewClass:RoomTitleView.class]; + ((RoomTitleView*)self.titleView).tapGestureDelegate = self; + MXKImageView *userPictureView = ((RoomTitleView*)self.titleView).pictureView; // Set user picture in input toolbar From c1f364580a1fdfadd0b980df8dfb89df5c179240 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Tue, 30 Nov 2021 01:14:05 +0300 Subject: [PATCH 75/87] Fix alert --- Riot/Modules/Room/RoomViewController.m | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 3cb0e4c3d..e6b30942b 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -3214,9 +3214,9 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; && !selectedEvent.threadId; if (showThreadOption && [self canCopyEvent:selectedEvent andCell:cell]) { - [currentAlert addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionCopy] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { + [actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionCopy] + style:UIAlertActionStyleDefault + handler:^(UIAlertAction * action) { MXStrongifyAndReturnIfNil(self); [self cancelEventSelection]; From 1f92c80897596e487483cacbfbcf141d89519286 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Tue, 30 Nov 2021 01:29:52 +0300 Subject: [PATCH 76/87] Make remove actions destructive & move remove action to the bottom --- Riot/Assets/en.lproj/Vector.strings | 2 +- Riot/Generated/Strings.swift | 2 +- Riot/Modules/Room/RoomViewController.m | 58 +++++++++++++------------- 3 files changed, 31 insertions(+), 31 deletions(-) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 89280cda5..397b981cb 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -363,7 +363,7 @@ Tap the + to start adding people."; "room_event_action_share" = "Share"; "room_event_action_forward" = "Forward"; "room_event_action_view_in_room" = "View in room"; -"room_event_action_permalink" = "Permalink"; +"room_event_action_permalink" = "Copy link to message"; "room_event_action_view_source" = "View Source"; "room_event_action_view_decrypted_source" = "View Decrypted Source"; "room_event_action_report" = "Report content"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index b7917aa7b..a827ff895 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -2931,7 +2931,7 @@ public class VectorL10n: NSObject { public static var roomEventActionMore: String { return VectorL10n.tr("Vector", "room_event_action_more") } - /// Permalink + /// Copy link to message public static var roomEventActionPermalink: String { return VectorL10n.tr("Vector", "room_event_action_permalink") } diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index e6b30942b..2267fdf87 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -3240,7 +3240,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; }]]; [actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionDelete] - style:UIAlertActionStyleDefault + style:UIAlertActionStyleDestructive handler:^(UIAlertAction * action) { MXStrongifyAndReturnIfNil(self); @@ -3506,34 +3506,6 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; } } - // Do not allow to redact the event that enabled encryption (m.room.encryption) - // because it breaks everything - if (selectedEvent.eventType != MXEventTypeRoomEncryption) - { - [actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionRedact] - style:UIAlertActionStyleDefault - handler:^(UIAlertAction * action) { - MXStrongifyAndReturnIfNil(self); - - [self cancelEventSelection]; - - [self startActivityIndicator]; - - MXWeakify(self); - [self.roomDataSource.room redactEvent:selectedEvent.eventId reason:nil success:^{ - MXStrongifyAndReturnIfNil(self); - [self stopActivityIndicator]; - } failure:^(NSError *error) { - MXStrongifyAndReturnIfNil(self); - [self stopActivityIndicator]; - - MXLogDebug(@"[RoomVC] Redact event (%@) failed", selectedEvent.eventId); - //Alert user - [self showError:error]; - }]; - }]]; - } - if (BuildSettings.messageDetailsAllowPermalink) { [actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionPermalink] @@ -3707,6 +3679,34 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [self showEncryptionInformation:selectedEvent]; }]]; } + + // Do not allow to redact the event that enabled encryption (m.room.encryption) + // because it breaks everything + if (selectedEvent.eventType != MXEventTypeRoomEncryption) + { + [actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionRedact] + style:UIAlertActionStyleDestructive + handler:^(UIAlertAction * action) { + MXStrongifyAndReturnIfNil(self); + + [self cancelEventSelection]; + + [self startActivityIndicator]; + + MXWeakify(self); + [self.roomDataSource.room redactEvent:selectedEvent.eventId reason:nil success:^{ + MXStrongifyAndReturnIfNil(self); + [self stopActivityIndicator]; + } failure:^(NSError *error) { + MXStrongifyAndReturnIfNil(self); + [self stopActivityIndicator]; + + MXLogDebug(@"[RoomVC] Redact event (%@) failed", selectedEvent.eventId); + //Alert user + [self showError:error]; + }]; + }]]; + } } [actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n cancel] From 6090d4c57f12e41342f7d05f5d95a30f7ec263de Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Tue, 30 Nov 2021 01:45:20 +0300 Subject: [PATCH 77/87] Fix thread property --- Riot/Modules/Threads/ThreadsCoordinator.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Modules/Threads/ThreadsCoordinator.swift b/Riot/Modules/Threads/ThreadsCoordinator.swift index 6b2cee444..807d6d125 100644 --- a/Riot/Modules/Threads/ThreadsCoordinator.swift +++ b/Riot/Modules/Threads/ThreadsCoordinator.swift @@ -122,7 +122,7 @@ final class ThreadsCoordinator: NSObject, ThreadsCoordinatorProtocol { session: parameters.session, roomId: parameters.roomId, eventId: nil, - threadId: thread.identifier, + threadId: thread.id, displayConfiguration: .forThreads) let coordinator = RoomCoordinator(parameters: parameters) coordinator.delegate = self From 786f7b60a9bcb6cdb8872923fd6888f7fa2cebed Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Tue, 30 Nov 2021 14:13:57 +0300 Subject: [PATCH 78/87] Just highlight event when possible instead of reloading data source --- Riot/Modules/Room/RoomCoordinator.swift | 2 +- Riot/Modules/Room/RoomViewController.h | 8 +++++++ Riot/Modules/Room/RoomViewController.m | 29 ++++++++++++++++++++++--- 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/Riot/Modules/Room/RoomCoordinator.swift b/Riot/Modules/Room/RoomCoordinator.swift index c7e24220a..ecf7e15b6 100644 --- a/Riot/Modules/Room/RoomCoordinator.swift +++ b/Riot/Modules/Room/RoomCoordinator.swift @@ -132,7 +132,7 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { self.selectedEventId = eventId if self.hasStartedOnce { - self.loadRoom(withId: self.parameters.roomId, andEventId: eventId, completion: completion) + self.roomViewController.highlightEvent(eventId, completion: completion) } else { self.start(withCompletion: completion) } diff --git a/Riot/Modules/Room/RoomViewController.h b/Riot/Modules/Room/RoomViewController.h index aec84d4bc..5cb835b06 100644 --- a/Riot/Modules/Room/RoomViewController.h +++ b/Riot/Modules/Room/RoomViewController.h @@ -100,6 +100,14 @@ extern NSNotificationName const RoomGroupCallTileTappedNotification; - (IBAction)scrollToBottomAction:(id)sender; +/** + Highlights an event in the timeline. Does not reload room data source if the event is already loaded. Otherwise, loads a new data source around the given event. + + @param eventId Identifier of the event to be highlighted. + @param completion Completion block to be called at the end of process. Optional. + */ +- (void)highlightEvent:(NSString *)eventId completion:(nullable void (^)(void))completion; + /** Creates and returns a new `RoomViewController` object. diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 0ea9bf192..542d8a2b6 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -6378,11 +6378,30 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [self.threadBridgePresenter pushFrom:self.navigationController animated:YES]; } -- (void)highlightEvent:(NSString *)eventId +- (void)highlightEvent:(NSString *)eventId completion:(void (^)(void))completion { NSInteger row = [self.roomDataSource indexOfCellDataWithEventId:eventId]; if (row == NSNotFound) { + // event with eventId is not loaded into data source yet, load another data source and display it + [self startActivityIndicator]; + MXWeakify(self); + [RoomDataSource loadRoomDataSourceWithRoomId:self.roomDataSource.roomId + initialEventId:eventId + threadId:nil + andMatrixSession:self.roomDataSource.mxSession + onComplete:^(RoomDataSource *roomDataSource) { + MXStrongifyAndReturnIfNil(self); + [self stopActivityIndicator]; + roomDataSource.markTimelineInitialEvent = YES; + [self displayRoom:roomDataSource]; + // Give the data source ownership to the room view controller. + self.hasRoomDataSourceOwnership = YES; + if (completion) + { + completion(); + } + }]; return; } @@ -6400,6 +6419,10 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; atScrollPosition:UITableViewScrollPositionMiddle animated:YES]; } + if (completion) + { + completion(); + } } #pragma mark - RoomContextualMenuViewControllerDelegate @@ -6825,7 +6848,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [self.threadBridgePresenter dismissWithAnimated:YES completion:^{ MXStrongifyAndReturnIfNil(self); - [self highlightEvent:eventId]; + [self highlightEvent:eventId completion:nil]; }]; } } @@ -6853,7 +6876,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; if (eventId) { - [self highlightEvent:eventId]; + [self highlightEvent:eventId completion:nil]; } }]; } From 98622ac41cba68bcc67aa95435539f8f04cf5037 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Wed, 1 Dec 2021 01:22:16 +0300 Subject: [PATCH 79/87] Navigate to thread view for search results in threads --- Riot/Assets/Base.lproj/Main.storyboard | 29 +------- .../Files/RoomFilesSearchViewController.h | 5 -- .../Files/RoomFilesSearchViewController.m | 12 ++-- .../RoomMessagesSearchViewController.h | 5 -- .../RoomMessagesSearchViewController.m | 12 ++-- .../Room/Search/RoomSearchViewController.h | 2 + .../Room/Search/RoomSearchViewController.m | 67 +++++++------------ 7 files changed, 39 insertions(+), 93 deletions(-) diff --git a/Riot/Assets/Base.lproj/Main.storyboard b/Riot/Assets/Base.lproj/Main.storyboard index 06340fad3..4708d7dbc 100644 --- a/Riot/Assets/Base.lproj/Main.storyboard +++ b/Riot/Assets/Base.lproj/Main.storyboard @@ -64,27 +64,6 @@ - - - - - - - - - - - - - - - - - - - - - @@ -98,9 +77,6 @@ - - - @@ -543,10 +519,10 @@ - + - + @@ -566,7 +542,6 @@ - diff --git a/Riot/Modules/Room/Search/Files/RoomFilesSearchViewController.h b/Riot/Modules/Room/Search/Files/RoomFilesSearchViewController.h index 3e2061098..8dc7f9ffa 100644 --- a/Riot/Modules/Room/Search/Files/RoomFilesSearchViewController.h +++ b/Riot/Modules/Room/Search/Files/RoomFilesSearchViewController.h @@ -18,9 +18,4 @@ @interface RoomFilesSearchViewController : MXKSearchViewController -/** - The event selected in the search results - */ -@property (nonatomic, readonly) MXEvent *selectedEvent; - @end diff --git a/Riot/Modules/Room/Search/Files/RoomFilesSearchViewController.m b/Riot/Modules/Room/Search/Files/RoomFilesSearchViewController.m index 0e14b53ee..a7d211c00 100644 --- a/Riot/Modules/Room/Search/Files/RoomFilesSearchViewController.m +++ b/Riot/Modules/Room/Search/Files/RoomFilesSearchViewController.m @@ -173,18 +173,16 @@ { // Data in the cells are actually Vector RoomBubbleCellData FilesSearchCellData *cellData = (FilesSearchCellData*)[self.dataSource cellDataAtIndex:indexPath.row]; - _selectedEvent = cellData.searchResult.result; + MXEvent *event = cellData.searchResult.result; + + RoomSearchViewController *roomSearchViewController = (RoomSearchViewController*)self.parentViewController; // Hide the keyboard handled by the search text input which belongs to RoomSearchViewController - [((RoomSearchViewController*)self.parentViewController).searchBar resignFirstResponder]; + [roomSearchViewController resignFirstResponder]; [tableView deselectRowAtIndexPath:indexPath animated:YES]; - // Make the RoomSearchViewController (that contains this VC) open the RoomViewController - [self.parentViewController performSegueWithIdentifier:@"showTimeline" sender:self]; - - // Reset the selected event. RoomSearchViewController got it when here - _selectedEvent = nil; + [roomSearchViewController selectEvent:event]; } @end diff --git a/Riot/Modules/Room/Search/Messages/RoomMessagesSearchViewController.h b/Riot/Modules/Room/Search/Messages/RoomMessagesSearchViewController.h index bff2f4439..e19120a45 100644 --- a/Riot/Modules/Room/Search/Messages/RoomMessagesSearchViewController.h +++ b/Riot/Modules/Room/Search/Messages/RoomMessagesSearchViewController.h @@ -18,9 +18,4 @@ @interface RoomMessagesSearchViewController : MXKSearchViewController -/** - The event selected in the search results - */ -@property (nonatomic, readonly) MXEvent *selectedEvent; - @end diff --git a/Riot/Modules/Room/Search/Messages/RoomMessagesSearchViewController.m b/Riot/Modules/Room/Search/Messages/RoomMessagesSearchViewController.m index 4e93f01d6..7a8c5f8cb 100644 --- a/Riot/Modules/Room/Search/Messages/RoomMessagesSearchViewController.m +++ b/Riot/Modules/Room/Search/Messages/RoomMessagesSearchViewController.m @@ -209,18 +209,16 @@ { // Data in the cells are actually Vector RoomBubbleCellData RoomBubbleCellData *cellData = (RoomBubbleCellData*)[self.dataSource cellDataAtIndex:indexPath.row]; - _selectedEvent = cellData.bubbleComponents[0].event; + MXEvent *event = cellData.bubbleComponents[0].event; + + RoomSearchViewController *roomSearchViewController = (RoomSearchViewController*)self.parentViewController; // Hide the keyboard handled by the search text input which belongs to RoomSearchViewController - [((RoomSearchViewController*)self.parentViewController).searchBar resignFirstResponder]; + [roomSearchViewController.searchBar resignFirstResponder]; [tableView deselectRowAtIndexPath:indexPath animated:YES]; - // Make the RoomSearchViewController (that contains this VC) open the RoomViewController - [self.parentViewController performSegueWithIdentifier:@"showTimeline" sender:self]; - - // Reset the selected event. RoomSearchViewController got it when here - _selectedEvent = nil; + [roomSearchViewController selectEvent:event]; } @end diff --git a/Riot/Modules/Room/Search/RoomSearchViewController.h b/Riot/Modules/Room/Search/RoomSearchViewController.h index 14c1460b5..8c4a9ca93 100644 --- a/Riot/Modules/Room/Search/RoomSearchViewController.h +++ b/Riot/Modules/Room/Search/RoomSearchViewController.h @@ -27,4 +27,6 @@ + (instancetype)instantiate; +- (void)selectEvent:(MXEvent *)event; + @end diff --git a/Riot/Modules/Room/Search/RoomSearchViewController.m b/Riot/Modules/Room/Search/RoomSearchViewController.m index 42315cf1e..b15288c3a 100644 --- a/Riot/Modules/Room/Search/RoomSearchViewController.m +++ b/Riot/Modules/Room/Search/RoomSearchViewController.m @@ -143,6 +143,31 @@ return ThemeService.shared.theme.statusBarStyle; } +- (void)selectEvent:(MXEvent *)event +{ + ThreadParameters *threadParameters = nil; + if (event.threadId) + { + threadParameters = [[ThreadParameters alloc] initWithThreadId:event.threadId + stackRoomScreen:NO]; + } + else if ([self.mainSession.threadingService isEventThreadRoot:event]) + { + threadParameters = [[ThreadParameters alloc] initWithThreadId:event.eventId + stackRoomScreen:NO]; + } + + ScreenPresentationParameters *screenParameters = [[ScreenPresentationParameters alloc] initWithRestoreInitialDisplay:NO + stackAboveVisibleViews:YES]; + + RoomNavigationParameters *parameters = [[RoomNavigationParameters alloc] initWithRoomId:event.roomId + eventId:event.eventId + mxSession:self.mainSession + threadParameters:threadParameters + presentationParameters:screenParameters]; + [[LegacyAppDelegate theDelegate] showRoomWithParameters:parameters]; +} + #pragma mark - - (void)setRoomDataSource:(MXKRoomDataSource *)roomDataSource @@ -294,48 +319,6 @@ [self updateSearch]; } -#pragma mark - Navigation - -- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender -{ - [super prepareForSegue:segue sender:sender]; - - if ([[segue identifier] isEqualToString:@"showTimeline"]) - { - // Check whether an event has been selected from messages or files search tab - MXEvent *selectedSearchEvent = messagesSearchViewController.selectedEvent; - MXSession *selectedSearchEventSession = messagesSearchDataSource.mxSession; - if (!selectedSearchEvent) - { - selectedSearchEvent = filesSearchViewController.selectedEvent; - selectedSearchEventSession = filesSearchDataSource.mxSession; - } - - if (selectedSearchEvent) - { - RoomViewController *roomViewController = segue.destinationViewController; - - [RoomDataSource loadRoomDataSourceWithRoomId:selectedSearchEvent.roomId - initialEventId:selectedSearchEvent.eventId - threadId:selectedSearchEvent.threadId - andMatrixSession:selectedSearchEventSession - onComplete:^(RoomDataSource *roomDataSource) { - - [roomDataSource finalizeInitialization]; - roomDataSource.markTimelineInitialEvent = YES; - - [roomViewController displayRoom:roomDataSource]; - roomViewController.hasRoomDataSourceOwnership = YES; - - roomViewController.navigationItem.leftItemsSupplementBackButton = YES; - }]; - } - - // Hide back button title - self.navigationItem.backBarButtonItem =[[UIBarButtonItem alloc] initWithTitle:@"" style:UIBarButtonItemStylePlain target:nil action:nil]; - } -} - #pragma mark - Search // Update search results under the currently selected tab From fbac2b031fddb2d768ee3b2fdeafbf6f1dae8730 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Wed, 1 Dec 2021 01:22:54 +0300 Subject: [PATCH 80/87] Add changelog --- changelog.d/5095.change | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/5095.change diff --git a/changelog.d/5095.change b/changelog.d/5095.change new file mode 100644 index 000000000..4a218fbeb --- /dev/null +++ b/changelog.d/5095.change @@ -0,0 +1 @@ +Search: Navigate to thread view for search results in threads. From 924d1d0e30de75d184a6bf1bca4abc212f8cbc22 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Thu, 9 Dec 2021 16:57:43 +0300 Subject: [PATCH 81/87] Apply MatrixKit PR changes --- Riot/Generated/Strings.swift | 10 +- .../Controllers/MXKRoomViewController.h | 7 ++ .../Models/Room/MXKRoomBubbleCellData.h | 5 + .../Models/Room/MXKRoomBubbleCellData.m | 16 +++ .../Models/Room/MXKRoomBubbleComponent.h | 7 ++ .../Models/Room/MXKRoomBubbleComponent.m | 9 ++ .../MatrixKit/Models/Room/MXKRoomDataSource.h | 32 +++++- .../MatrixKit/Models/Room/MXKRoomDataSource.m | 102 +++++++++++------- .../Utils/EventFormatter/MXKEventFormatter.m | 22 ++-- .../MXKRoomBubbleTableViewCell.h | 2 +- .../MXKRoomBubbleTableViewCell.m | 10 +- .../Title/Thread/ThreadRoomTitleView.swift | 1 - .../Modules/Settings/SettingsViewController.m | 2 + .../Coordinator/PollEditFormCoordinator.swift | 2 +- .../Coordinator/PollTimelineCoordinator.swift | 3 +- 15 files changed, 161 insertions(+), 69 deletions(-) diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index e9c6201c2..0f0cd08a3 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -4627,13 +4627,13 @@ public class VectorL10n: NSObject { public static var settingsLabsEnableRingingForGroupCalls: String { return VectorL10n.tr("Vector", "settings_labs_enable_ringing_for_group_calls") } - /// Polls - public static var settingsLabsEnabledPolls: String { - return VectorL10n.tr("Vector", "settings_labs_enabled_polls") - } /// Threaded messaging public static var settingsLabsEnableThreads: String { - return VectorL10n.tr("Vector", "settings_labs_enable_threads") + return VectorL10n.tr("Vector", "settings_labs_enable_threads") + } + /// Polls + public static var settingsLabsEnabledPolls: String { + return VectorL10n.tr("Vector", "settings_labs_enabled_polls") } /// React to messages with emoji public static var settingsLabsMessageReaction: String { diff --git a/Riot/Modules/MatrixKit/Controllers/MXKRoomViewController.h b/Riot/Modules/MatrixKit/Controllers/MXKRoomViewController.h index 8ae86eccb..45ad325b5 100644 --- a/Riot/Modules/MatrixKit/Controllers/MXKRoomViewController.h +++ b/Riot/Modules/MatrixKit/Controllers/MXKRoomViewController.h @@ -437,4 +437,11 @@ typedef NS_ENUM(NSUInteger, MXKRoomViewControllerJoinRoomResult) { */ - (void)setBubbleTableViewContentOffset:(CGPoint)contentOffset animated:(BOOL)animated; +/** + Handle typing notification. + + @param typing Flag indicating whether the user is typing or not. + */ +- (void)handleTypingNotification:(BOOL)typing; + @end diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.h b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.h index 691f092e6..e63d2f586 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.h +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.h @@ -80,6 +80,11 @@ */ @property (nonatomic, readonly) BOOL hasLink; +/** + Whether the data has a thread root in its components. + */ +@property (nonatomic, readonly) BOOL hasThreadRoot; + /** Event formatter */ diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.m index b20bcb668..b435b2f9d 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.m @@ -607,6 +607,22 @@ return NO; } +- (BOOL)hasThreadRoot +{ + @synchronized (bubbleComponents) + { + for (MXKRoomBubbleComponent *component in bubbleComponents) + { + if (component.thread) + { + return YES; + } + } + } + + return NO; +} + - (MXKRoomBubbleComponentDisplayFix)displayFix { MXKRoomBubbleComponentDisplayFix displayFix = MXKRoomBubbleComponentDisplayFixNone; diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleComponent.h b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleComponent.h index 3912932c8..b778785e6 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleComponent.h +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleComponent.h @@ -19,6 +19,8 @@ #import "MXKEventFormatter.h" #import "MXKURLPreviewDataProtocol.h" +@class MXThread; + /** Flags to indicate if a fix is required at the display time. */ @@ -103,6 +105,11 @@ typedef enum : NSUInteger { */ @property (nonatomic, readonly) BOOL showEncryptionBadge; +/** + Thread for the bubble component. Should only exist for thread root events. + */ +@property (nonatomic, readonly) MXThread *thread; + /** Create a new `MXKRoomBubbleComponent` object based on a `MXEvent` instance. diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleComponent.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleComponent.m index 565519ba9..e836253b6 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleComponent.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleComponent.m @@ -18,6 +18,13 @@ #import "MXEvent+MatrixKit.h" #import "MXKSwiftHeader.h" +#import + +@interface MXKRoomBubbleComponent () + +@property (nonatomic, readwrite) MXThread *thread; + +@end @implementation MXKRoomBubbleComponent @@ -62,6 +69,8 @@ _showEncryptionBadge = [self shouldShowWarningBadgeForEvent:event roomState:(MXRoomState*)roomState session:session]; [self updateLinkWithRoomState:roomState]; + + self.thread = [session.threadingService threadWithId:event.eventId]; } return self; } diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.h b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.h index 8559fba2d..5b7182f78 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.h +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.h @@ -169,6 +169,11 @@ extern NSString *const kMXKRoomDataSourceTimelineErrorErrorKey; */ @property (nonatomic) NSString *partialTextMessage; +/** + The current thread id for the data source. If provided, data source displays the specified thread, otherwise the whole room messages. + */ +@property (nonatomic, readonly) NSString *threadId; + #pragma mark - Configuration /** The text formatter applied on the events. @@ -269,10 +274,15 @@ extern NSString *const kMXKRoomDataSourceTimelineErrorErrorKey; @param roomId the id of the room to get data from. @param initialEventId the id of the event where to start the timeline. + @param threadId the id of the thread to load. @param mxSession the Matrix session to get data from. @param onComplete a block providing the newly created instance. */ -+ (void)loadRoomDataSourceWithRoomId:(NSString*)roomId initialEventId:(NSString*)initialEventId andMatrixSession:(MXSession*)mxSession onComplete:(void (^)(id roomDataSource))onComplete; ++ (void)loadRoomDataSourceWithRoomId:(NSString*)roomId + initialEventId:(NSString*)initialEventId + threadId:(NSString*)threadId + andMatrixSession:(MXSession*)mxSession + onComplete:(void (^)(id roomDataSource))onComplete; /** Asynchronously create a data source to peek into a room. @@ -306,10 +316,14 @@ extern NSString *const kMXKRoomDataSourceTimelineErrorErrorKey; @param roomId the id of the room to get data from. @param initialEventId the id of the event where to start the timeline. + @param threadId the id of the thread to initialize. @param mxSession the Matrix session to get data from. @return the newly created instance. */ -- (instancetype)initWithRoomId:(NSString*)roomId initialEventId:(NSString*)initialEventId andMatrixSession:(MXSession*)mxSession; +- (instancetype)initWithRoomId:(NSString*)roomId + initialEventId:(NSString*)initialEventId + threadId:(NSString*)threadId + andMatrixSession:(MXSession*)mxSession; /** Initialise the data source to peek into a room. @@ -678,6 +692,20 @@ extern NSString *const kMXKRoomDataSourceTimelineErrorErrorKey; */ + (dispatch_queue_t)processingQueue; +/** + Decides whether an event should be considered for asynchronous event processing. + Default implementation checks for `filterMessagesWithURL` and undecryptable events sent before the user joined. + Subclasses must call super at some point. + + @param event event to be processed or not + @param roomState the state of the room when the event fired + @param direction the direction of the event + @return YES to process the event, NO otherwise + */ +- (BOOL)shouldQueueEventForProcessing:(MXEvent*)event + roomState:(MXRoomState*)roomState + direction:(MXTimelineDirection)direction; + #pragma mark - Bubble collapsing /** diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m index 43ab3e623..d8545ed71 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m @@ -204,6 +204,7 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { @property (nonatomic, readwrite) MXRoom *secondaryRoom; @property (nonatomic, strong) MXEventTimeline *secondaryTimeline; +@property (nonatomic, readwrite) NSString *threadId; @end @@ -215,9 +216,9 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { [self ensureSessionStateForDataSource:roomDataSource initialEventId:nil andMatrixSession:mxSession onComplete:onComplete]; } -+ (void)loadRoomDataSourceWithRoomId:(NSString*)roomId initialEventId:(NSString*)initialEventId andMatrixSession:(MXSession*)mxSession onComplete:(void (^)(id roomDataSource))onComplete ++ (void)loadRoomDataSourceWithRoomId:(NSString*)roomId initialEventId:(NSString*)initialEventId threadId:(NSString*)threadId andMatrixSession:(MXSession*)mxSession onComplete:(void (^)(id roomDataSource))onComplete { - MXKRoomDataSource *roomDataSource = [[self alloc] initWithRoomId:roomId initialEventId:initialEventId andMatrixSession:mxSession]; + MXKRoomDataSource *roomDataSource = [[self alloc] initWithRoomId:roomId initialEventId:initialEventId threadId:threadId andMatrixSession:mxSession]; [self ensureSessionStateForDataSource:roomDataSource initialEventId:initialEventId andMatrixSession:mxSession onComplete:onComplete]; } @@ -347,7 +348,7 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { return self; } -- (instancetype)initWithRoomId:(NSString*)roomId initialEventId:(NSString*)initialEventId2 andMatrixSession:(MXSession*)mxSession +- (instancetype)initWithRoomId:(NSString*)roomId initialEventId:(NSString*)initialEventId2 threadId:(NSString*)threadId andMatrixSession:(MXSession*)mxSession { self = [self initWithRoomId:roomId andMatrixSession:mxSession]; if (self) @@ -357,6 +358,7 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { initialEventId = initialEventId2; _isLive = NO; } + _threadId = threadId; } return self; @@ -364,7 +366,7 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { - (instancetype)initWithPeekingRoom:(MXPeekingRoom*)peekingRoom2 andInitialEventId:(NSString*)theInitialEventId { - self = [self initWithRoomId:peekingRoom2.roomId initialEventId:theInitialEventId andMatrixSession:peekingRoom2.mxSession]; + self = [self initWithRoomId:peekingRoom2.roomId initialEventId:theInitialEventId threadId:nil andMatrixSession:peekingRoom2.mxSession]; if (self) { peekingRoom = peekingRoom2; @@ -1289,6 +1291,34 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { } +- (BOOL)shouldQueueEventForProcessing:(MXEvent*)event roomState:(MXRoomState*)roomState direction:(MXTimelineDirection)direction +{ + if (self.filterMessagesWithURL) + { + // Check whether the event has a value for the 'url' key in its content. + if (!event.getMediaURLs.count) + { + // ignore the event + return NO; + } + } + + // Check for undecryptable messages that were sent while the user was not in the room and hide them + if ([MXKAppSettings standardAppSettings].hidePreJoinedUndecryptableEvents + && direction == MXTimelineDirectionBackwards) + { + [self checkForPreJoinUTDWithEvent:event roomState:roomState]; + + // Hide pre joint UTD events + if (self.shouldStopBackPagination) + { + return NO; + } + } + + return YES; +} + #pragma mark - KVO - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context @@ -1425,7 +1455,11 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { // Launch the pagination MXWeakify(self); - paginationRequest = [_timeline paginate:numItems direction:direction onlyFromStore:onlyFromStore complete:^{ + paginationRequest = [_timeline paginate:numItems + direction:direction + onlyFromStore:onlyFromStore +// threadId:_threadId + complete:^{ MXStrongifyAndReturnIfNil(self); @@ -1489,7 +1523,11 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { dispatch_group_enter(dispatchGroup); // Launch the pagination MXWeakify(self); - secondaryPaginationRequest = [_secondaryTimeline paginate:numItems direction:direction onlyFromStore:onlyFromStore complete:^{ + secondaryPaginationRequest = [_secondaryTimeline paginate:numItems + direction:direction + onlyFromStore:onlyFromStore +// threadId:_threadId + complete:^{ MXStrongifyAndReturnIfNil(self); @@ -1676,11 +1714,11 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { // Make the request to the homeserver if (isEmote) { - [_room sendEmote:sanitizedText formattedText:html localEcho:&localEchoEvent success:success failure:failure]; + [_room sendEmote:sanitizedText formattedText:html threadId:self.threadId localEcho:&localEchoEvent success:success failure:failure]; } else { - [_room sendTextMessage:sanitizedText formattedText:html localEcho:&localEchoEvent success:success failure:failure]; + [_room sendTextMessage:sanitizedText formattedText:html threadId:self.threadId localEcho:&localEchoEvent success:success failure:failure]; } if (localEchoEvent) @@ -1821,7 +1859,7 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { { __block MXEvent *localEchoEvent = nil; - [_room sendImage:imageData withImageSize:imageSize mimeType:mimetype andThumbnail:thumbnail localEcho:&localEchoEvent success:success failure:failure]; + [_room sendImage:imageData withImageSize:imageSize mimeType:mimetype andThumbnail:thumbnail threadId:self.threadId localEcho:&localEchoEvent success:success failure:failure]; if (localEchoEvent) { @@ -1841,7 +1879,7 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { { __block MXEvent *localEchoEvent = nil; - [_room sendVideoAsset:videoAsset withThumbnail:videoThumbnail localEcho:&localEchoEvent success:success failure:failure]; + [_room sendVideoAsset:videoAsset withThumbnail:videoThumbnail threadId:self.threadId localEcho:&localEchoEvent success:success failure:failure]; if (localEchoEvent) { @@ -1855,7 +1893,7 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { { __block MXEvent *localEchoEvent = nil; - [_room sendAudioFile:audioFileLocalURL mimeType:mimeType localEcho:&localEchoEvent success:success failure:failure keepActualFilename:YES]; + [_room sendAudioFile:audioFileLocalURL mimeType:mimeType threadId:self.threadId localEcho:&localEchoEvent success:success failure:failure keepActualFilename:YES]; if (localEchoEvent) { @@ -1874,7 +1912,7 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { { __block MXEvent *localEchoEvent = nil; - [_room sendVoiceMessage:audioFileLocalURL mimeType:mimeType duration:duration samples:samples localEcho:&localEchoEvent success:success failure:failure keepActualFilename:YES]; + [_room sendVoiceMessage:audioFileLocalURL mimeType:mimeType duration:duration samples:samples threadId:self.threadId localEcho:&localEchoEvent success:success failure:failure keepActualFilename:YES]; if (localEchoEvent) { @@ -1889,7 +1927,7 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { { __block MXEvent *localEchoEvent = nil; - [_room sendFile:fileLocalURL mimeType:mimeType localEcho:&localEchoEvent success:success failure:failure]; + [_room sendFile:fileLocalURL mimeType:mimeType threadId:self.threadId localEcho:&localEchoEvent success:success failure:failure]; if (localEchoEvent) { @@ -1904,7 +1942,7 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { __block MXEvent *localEchoEvent = nil; // Make the request to the homeserver - [_room sendMessageWithContent:msgContent localEcho:&localEchoEvent success:success failure:failure]; + [_room sendMessageWithContent:msgContent threadId:self.threadId localEcho:&localEchoEvent success:success failure:failure]; if (localEchoEvent) { @@ -1919,7 +1957,7 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { __block MXEvent *localEchoEvent = nil; // Make the request to the homeserver - [_room sendEventOfType:eventTypeString content:msgContent localEcho:&localEchoEvent success:success failure:failure]; + [_room sendEventOfType:eventTypeString content:msgContent threadId:self.threadId localEcho:&localEchoEvent success:success failure:failure]; if (localEchoEvent) { @@ -1946,7 +1984,7 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { { // We try here to resent an encrypted event // Note: we keep the existing local echo. - [_room sendEventOfType:kMXEventTypeStringRoomEncrypted content:event.wireContent localEcho:&event success:success failure:failure]; + [_room sendEventOfType:kMXEventTypeStringRoomEncrypted content:event.wireContent threadId:self.threadId localEcho:&event success:success failure:failure]; } else if ([event.type isEqualToString:kMXEventTypeStringRoomMessage]) { @@ -1955,7 +1993,7 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { if ([msgType isEqualToString:kMXMessageTypeText] || [msgType isEqualToString:kMXMessageTypeEmote]) { // Resend the Matrix event by reusing the existing echo - [_room sendMessageWithContent:event.content localEcho:&event success:success failure:failure]; + [_room sendMessageWithContent:event.content threadId:self.threadId localEcho:&event success:success failure:failure]; } else if ([msgType isEqualToString:kMXMessageTypeImage]) { @@ -1998,7 +2036,7 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { else { // Resend the Matrix event by reusing the existing echo - [_room sendMessageWithContent:event.content localEcho:&event success:success failure:failure]; + [_room sendMessageWithContent:event.content threadId:self.threadId localEcho:&event success:success failure:failure]; } } else if ([msgType isEqualToString:kMXMessageTypeAudio]) @@ -2009,7 +2047,7 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { if (!contentURL || ![contentURL hasPrefix:kMXMediaUploadIdPrefix]) { // Resend the Matrix event by reusing the existing echo - [_room sendMessageWithContent:event.content localEcho:&event success:success failure:failure]; + [_room sendMessageWithContent:event.content threadId:self.threadId localEcho:&event success:success failure:failure]; return; } @@ -2049,7 +2087,7 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { else { // Resend the Matrix event by reusing the existing echo - [_room sendMessageWithContent:event.content localEcho:&event success:success failure:failure]; + [_room sendMessageWithContent:event.content threadId:self.threadId localEcho:&event success:success failure:failure]; } } else if ([msgType isEqualToString:kMXMessageTypeFile]) @@ -2085,7 +2123,7 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { else { // Resend the Matrix event by reusing the existing echo - [_room sendMessageWithContent:event.content localEcho:&event success:success failure:failure]; + [_room sendMessageWithContent:event.content threadId:self.threadId localEcho:&event success:success failure:failure]; } } else @@ -2782,27 +2820,9 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { MXLogVerbose(@"[MXKRoomDataSource][%p] queueEventForProcessing: %@", self, event.eventId); } - if (self.filterMessagesWithURL) + if (![self shouldQueueEventForProcessing:event roomState:roomState direction:direction]) { - // Check whether the event has a value for the 'url' key in its content. - if (!event.getMediaURLs.count) - { - // Ignore the event - return; - } - } - - // Check for undecryptable messages that were sent while the user was not in the room and hide them - if ([MXKAppSettings standardAppSettings].hidePreJoinedUndecryptableEvents - && direction == MXTimelineDirectionBackwards) - { - [self checkForPreJoinUTDWithEvent:event roomState:roomState]; - - // Hide pre joint UTD events - if (self.shouldStopBackPagination) - { - return; - } + return; } MXKQueuedEvent *queuedEvent = [[MXKQueuedEvent alloc] initWithEvent:event andRoomState:roomState direction:direction]; diff --git a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m index 9177cc5be..4dd03ae5c 100644 --- a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m +++ b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m @@ -329,11 +329,11 @@ static NSString *const kHTMLATagRegexPattern = @"([^<]*)"; // Check first whether the event has been redacted NSString *redactedInfo = nil; - BOOL isRedacted = (event.redactedBecause != nil); + BOOL isRedacted = event.isRedactedEvent; if (isRedacted) { - // Check whether redacted information is required - if (_settings.showRedactionsInRoomHistory) + // Check whether the event is a thread root or redacted information is required + if ([mxSession.threadingService isEventThreadRoot:event] || _settings.showRedactionsInRoomHistory) { MXLogDebug(@"[MXKEventFormatter] Redacted event %@ (%@)", event.description, event.redactedBecause); @@ -1256,7 +1256,7 @@ static NSString *const kHTMLATagRegexPattern = @"([^<]*)"; NSString *body; BOOL isHTML = NO; - NSString *eventThreadIdentifier = event.threadIdentifier; + NSString *eventThreadId = event.threadId; // Use the HTML formatted string if provided if ([event.content[@"format"] isEqualToString:kMXRoomMessageFormatHTML]) @@ -1264,17 +1264,17 @@ static NSString *const kHTMLATagRegexPattern = @"([^<]*)"; isHTML =YES; MXJSONModelSetString(body, event.content[@"formatted_body"]); } - else if (eventThreadIdentifier) + else if (eventThreadId && !RiotSettings.shared.enableThreads) { isHTML = YES; MXJSONModelSetString(body, event.content[@"body"]); - MXEvent *threadRootEvent = [mxSession.store eventWithEventId:eventThreadIdentifier + MXEvent *threadRootEvent = [mxSession.store eventWithEventId:eventThreadId inRoom:event.roomId]; NSString *threadRootEventContent; MXJSONModelSetString(threadRootEventContent, threadRootEvent.content[@"body"]); body = [NSString stringWithFormat:@"
In reply to %@
%@
%@", - [MXTools permalinkToEvent:eventThreadIdentifier inRoom:event.roomId], + [MXTools permalinkToEvent:eventThreadId inRoom:event.roomId], [MXTools permalinkToUserWithUserId:threadRootEvent.sender], threadRootEvent.sender, threadRootEventContent, @@ -1382,7 +1382,7 @@ static NSString *const kHTMLATagRegexPattern = @"([^<]*)"; // This helps us insert the emote prefix in the right place NSDictionary *relatesTo; MXJSONModelSetDictionary(relatesTo, event.content[@"m.relates_to"]); - if ([relatesTo[@"m.in_reply_to"] isKindOfClass:NSDictionary.class] || event.isInThread) + if ([relatesTo[@"m.in_reply_to"] isKindOfClass:NSDictionary.class] || (event.isInThread && !RiotSettings.shared.enableThreads)) { [attributedDisplayText enumerateAttribute:kMXKToolsBlockquoteMarkAttribute inRange:NSMakeRange(0, attributedDisplayText.length) @@ -1720,7 +1720,7 @@ static NSString *const kHTMLATagRegexPattern = @"([^<]*)"; NSString *html = htmlString; // Special treatment for "In reply to" message - if (event.isReplyEvent || event.isInThread) + if (event.isReplyEvent || (event.isInThread && !RiotSettings.shared.enableThreads)) { html = [self renderReplyTo:html withRoomState:roomState]; } @@ -2046,7 +2046,7 @@ static NSString *const kHTMLATagRegexPattern = @"([^<]*)"; textColor = _errorTextColor; } // Check whether the message is highlighted. - else if (event.mxkIsHighlighted || (event.isInThread && ![event.sender isEqualToString:mxSession.myUserId])) + else if (event.mxkIsHighlighted || (event.isInThread && !RiotSettings.shared.enableThreads && ![event.sender isEqualToString:mxSession.myUserId])) { textColor = _bingTextColor; } @@ -2110,7 +2110,7 @@ static NSString *const kHTMLATagRegexPattern = @"([^<]*)"; { font = _callNoticesTextFont; } - else if (event.mxkIsHighlighted || (event.isInThread && ![event.sender isEqualToString:mxSession.myUserId])) + else if (event.mxkIsHighlighted || (event.isInThread && !RiotSettings.shared.enableThreads && ![event.sender isEqualToString:mxSession.myUserId])) { font = _bingTextFont; } diff --git a/Riot/Modules/MatrixKit/Views/RoomBubbleList/MXKRoomBubbleTableViewCell.h b/Riot/Modules/MatrixKit/Views/RoomBubbleList/MXKRoomBubbleTableViewCell.h index 60cad3248..733d98141 100644 --- a/Riot/Modules/MatrixKit/Views/RoomBubbleList/MXKRoomBubbleTableViewCell.h +++ b/Riot/Modules/MatrixKit/Views/RoomBubbleList/MXKRoomBubbleTableViewCell.h @@ -168,7 +168,7 @@ extern NSString *const kMXKRoomBubbleCellUrlItemInteraction; @property (nonatomic) UIImage *picturePlaceholder; /** - The list of the temporary subviews that should be removed before reusing the cell (nil by default). + The list of the temporary subviews that should be removed before reusing the cell (empty array by default). */ @property (nonatomic) NSMutableArray *tmpSubviews; diff --git a/Riot/Modules/MatrixKit/Views/RoomBubbleList/MXKRoomBubbleTableViewCell.m b/Riot/Modules/MatrixKit/Views/RoomBubbleList/MXKRoomBubbleTableViewCell.m index b550a1ef1..173606b09 100644 --- a/Riot/Modules/MatrixKit/Views/RoomBubbleList/MXKRoomBubbleTableViewCell.m +++ b/Riot/Modules/MatrixKit/Views/RoomBubbleList/MXKRoomBubbleTableViewCell.m @@ -121,6 +121,7 @@ static BOOL _disableLongPressGestureOnEvent; self.readReceiptsAlignment = ReadReceiptAlignmentLeft; _allTextHighlighted = NO; _isAutoAnimatedGif = NO; + _tmpSubviews = [NSMutableArray array]; } - (void)awakeFromNib @@ -1005,14 +1006,11 @@ static BOOL _disableLongPressGestureOnEvent; self.bubbleInfoContainer.hidden = YES; // Remove temporary subviews - if (self.tmpSubviews) + for (UIView *view in self.tmpSubviews) { - for (UIView *view in self.tmpSubviews) - { - [view removeFromSuperview]; - } - self.tmpSubviews = nil; + [view removeFromSuperview]; } + [self.tmpSubviews removeAllObjects]; // Remove potential overlay subviews if (self.bubbleOverlayContainer) diff --git a/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.swift b/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.swift index 5795fac4b..3dcb7f905 100644 --- a/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.swift +++ b/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.swift @@ -15,7 +15,6 @@ // import Foundation -import MatrixKit @objc enum ThreadRoomTitleViewMode: Int { diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index c9fe4d57d..365d915d1 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -2426,6 +2426,8 @@ TableViewSectionsDelegate> labelAndSwitchCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor; [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleEnableThreads:) forControlEvents:UIControlEventValueChanged]; + + cell = labelAndSwitchCell; } } else if (section == SECTION_TAG_FLAIR) diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift b/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift index da63d7098..a8153bbbe 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift @@ -85,7 +85,7 @@ final class PollEditFormCoordinator: Coordinator { self.pollEditFormViewModel.dispatch(action: .startLoading) - self.parameters.room.sendPollStart(withContent: pollStartContent, localEcho: nil) { [weak self] result in + self.parameters.room.sendPollStart(withContent: pollStartContent, threadId: nil, localEcho: nil) { [weak self] result in guard let self = self else { return } self.parameters.navigationRouter?.dismissModule(animated: true, completion: nil) diff --git a/RiotSwiftUI/Modules/Room/PollTimeline/Coordinator/PollTimelineCoordinator.swift b/RiotSwiftUI/Modules/Room/PollTimeline/Coordinator/PollTimelineCoordinator.swift index 8d201264f..6ceb24c9b 100644 --- a/RiotSwiftUI/Modules/Room/PollTimeline/Coordinator/PollTimelineCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/PollTimeline/Coordinator/PollTimelineCoordinator.swift @@ -72,6 +72,7 @@ final class PollTimelineCoordinator: Coordinator, PollAggregatorDelegate { self.parameters.room.sendPollResponse(for: parameters.pollStartEvent, withAnswerIdentifiers: identifiers, + threadId: nil, localEcho: nil, success: nil) { [weak self] error in guard let self = self else { return } @@ -97,7 +98,7 @@ final class PollTimelineCoordinator: Coordinator, PollAggregatorDelegate { } func endPoll() { - parameters.room.sendPollEnd(for: parameters.pollStartEvent, localEcho: nil, success: nil) { [weak self] error in + parameters.room.sendPollEnd(for: parameters.pollStartEvent, threadId: nil, localEcho: nil, success: nil) { [weak self] error in self?.pollTimelineViewModel.dispatch(action: .showClosingFailure) } } From 759b841bff4c51cce3bb47af937c83d1433e4d68 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Thu, 9 Dec 2021 16:58:20 +0300 Subject: [PATCH 82/87] Display threaded events as replies if threads disabled, fix #5266 --- Riot/Modules/Room/DataSources/RoomDataSource.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m index aa3313473..c2d5db7ae 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.m +++ b/Riot/Modules/Room/DataSources/RoomDataSource.m @@ -266,7 +266,7 @@ const CGFloat kTypingCellHeight = 24; } } } - else + else if (RiotSettings.shared.enableThreads) { // if not in a thread, ignore all threaded events if (event.threadId) From 3ccd36718af42f84837a922d430df0d9c0d0ec02 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Mon, 17 Jan 2022 11:54:36 +0300 Subject: [PATCH 83/87] Add threadId parameter --- Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m index 6ecde5ebc..22df94be1 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m @@ -1964,6 +1964,7 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { [_room sendLocationWithLatitude:latitude longitude:longitude description:description + threadId:self.threadId localEcho:&localEchoEvent success:success failure:failure]; From b8d973d64d543678bb6061e98dafb38cf7565120 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Tue, 18 Jan 2022 02:14:23 +0300 Subject: [PATCH 84/87] Reset detail screen after lab setting change, fixes #5273 --- Riot/Modules/Settings/SettingsViewController.m | 1 + 1 file changed, 1 insertion(+) diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 5c902c44b..0e8fc02aa 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -3230,6 +3230,7 @@ TableViewSectionsDelegate> { RiotSettings.shared.enableThreads = sender.isOn; [[MXKRoomDataSourceManager sharedManagerForMatrixSession:self.mainSession] reset]; + [[AppDelegate theDelegate] restoreEmptyDetailsViewController]; } - (void)togglePinRoomsWithMissedNotif:(UISwitch *)sender From 48b99d6a2aa8588d6c64a0f00978e308ae68f011 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Tue, 25 Jan 2022 14:31:19 +0300 Subject: [PATCH 85/87] Implement thread summary view on cell decorator --- .../PlainRoomTimelineCellDecorator.swift | 45 +++++++++++++++++++ .../Styles/RoomTimelineCellDecorator.swift | 6 +++ 2 files changed, 51 insertions(+) diff --git a/Riot/Modules/Room/Views/BubbleCells/Styles/Plain/PlainRoomTimelineCellDecorator.swift b/Riot/Modules/Room/Views/BubbleCells/Styles/Plain/PlainRoomTimelineCellDecorator.swift index d684267e0..20b18a6a6 100644 --- a/Riot/Modules/Room/Views/BubbleCells/Styles/Plain/PlainRoomTimelineCellDecorator.swift +++ b/Riot/Modules/Room/Views/BubbleCells/Styles/Plain/PlainRoomTimelineCellDecorator.swift @@ -142,6 +142,51 @@ class PlainRoomTimelineCellDecorator: RoomTimelineCellDecorator { } } + func addThreadSummaryView(_ threadSummaryView: ThreadSummaryView, + toCell cell: MXKRoomBubbleTableViewCell, + cellData: RoomBubbleCellData, + contentViewPositionY: CGFloat, + upperDecorationView: UIView?) { + + cell.addTemporarySubview(threadSummaryView) + + threadSummaryView.translatesAutoresizingMaskIntoConstraints = false + + let cellContentView = cell.contentView + + cellContentView.addSubview(threadSummaryView) + + var leftMargin = RoomBubbleCellLayout.reactionsViewLeftMargin + + if cellData.containsBubbleComponentWithEncryptionBadge { + leftMargin += RoomBubbleCellLayout.encryptedContentLeftMargin + } + + let rightMargin = RoomBubbleCellLayout.reactionsViewRightMargin + let topMargin = RoomBubbleCellLayout.threadSummaryViewTopMargin + let height = ThreadSummaryView.contentViewHeight(forThread: threadSummaryView.thread, + fitting: cellData.maxTextViewWidth) + + // The top constraint may need to include the URL preview view + let topConstraint: NSLayoutConstraint + if let upperDecorationView = upperDecorationView { + topConstraint = threadSummaryView.topAnchor.constraint(equalTo: upperDecorationView.bottomAnchor, + constant: topMargin) + } else { + topConstraint = threadSummaryView.topAnchor.constraint(equalTo: cellContentView.topAnchor, + constant: contentViewPositionY + topMargin) + } + + NSLayoutConstraint.activate([ + threadSummaryView.leadingAnchor.constraint(equalTo: cellContentView.leadingAnchor, + constant: leftMargin), + threadSummaryView.trailingAnchor.constraint(lessThanOrEqualTo: cellContentView.trailingAnchor, + constant: -rightMargin), + threadSummaryView.heightAnchor.constraint(equalToConstant: height), + topConstraint + ]) + } + func addSendStatusView(toCell cell: MXKRoomBubbleTableViewCell, withFailedEventIds failedEventIds: Set) { cell.updateTickView(withFailedEventIds: failedEventIds) } diff --git a/Riot/Modules/Room/Views/BubbleCells/Styles/RoomTimelineCellDecorator.swift b/Riot/Modules/Room/Views/BubbleCells/Styles/RoomTimelineCellDecorator.swift index 89a665fe8..ec60bcbf4 100644 --- a/Riot/Modules/Room/Views/BubbleCells/Styles/RoomTimelineCellDecorator.swift +++ b/Riot/Modules/Room/Views/BubbleCells/Styles/RoomTimelineCellDecorator.swift @@ -41,6 +41,12 @@ protocol RoomTimelineCellDecorator { cellData: RoomBubbleCellData, contentViewPositionY: CGFloat, upperDecorationView: UIView?) + + func addThreadSummaryView(_ threadSummaryView: ThreadSummaryView, + toCell cell: MXKRoomBubbleTableViewCell, + cellData: RoomBubbleCellData, + contentViewPositionY: CGFloat, + upperDecorationView: UIView?) func addSendStatusView(toCell cell: MXKRoomBubbleTableViewCell, withFailedEventIds failedEventIds: Set) From 785fe6b4e0905acb23f55b9fb3ca5d4cef408491 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Tue, 25 Jan 2022 23:59:19 +0300 Subject: [PATCH 86/87] Fix review remarks --- Riot/Assets/en.lproj/Vector.strings | 2 +- Riot/Modules/Application/LegacyAppDelegate.m | 4 +- .../Common/Recents/RecentsViewController.m | 2 +- .../Files/HomeFilesSearchViewController.m | 2 +- .../HomeMessagesSearchViewController.m | 2 +- .../Rooms/DirectoryViewController.m | 2 +- .../MatrixKit/Models/Room/MXKRoomDataSource.h | 4 +- .../MatrixKit/Models/Room/MXKRoomDataSource.m | 2 - .../Modules/Room/DataSources/RoomDataSource.h | 4 +- .../Modules/Room/DataSources/RoomDataSource.m | 1 + Riot/Modules/Room/RoomCoordinator.swift | 2 +- Riot/Modules/Room/RoomViewController.h | 2 +- Riot/Modules/Room/RoomViewController.m | 86 ++++---------- ...ewModel.swift => ThreadSummaryModel.swift} | 8 +- .../Views/Threads/ThreadSummaryView.swift | 10 +- ...Model.swift => ThreadRoomTitleModel.swift} | 14 +-- .../Title/Thread/ThreadRoomTitleView.swift | 8 +- .../SplitView/SplitViewCoordinator.swift | 4 +- .../SplitView/SplitViewPresentable.swift | 4 +- Riot/Modules/TabBar/TabBarCoordinator.swift | 112 ++++++++++-------- .../ThreadListCoordinatorProtocol.swift | 2 +- .../ThreadList/ThreadListViewController.swift | 8 +- .../ThreadList/ThreadListViewModel.swift | 54 ++++----- .../ThreadListViewModelProtocol.swift | 4 +- .../ThreadList/ThreadListViewState.swift | 2 +- ...hreadViewModel.swift => ThreadModel.swift} | 12 +- .../Views/Cell/ThreadTableViewCell.swift | 2 +- ...Model.swift => ThreadListEmptyModel.swift} | 2 +- .../Views/Empty/ThreadListEmptyView.swift | 3 +- Riot/Modules/Threads/ThreadsCoordinator.swift | 17 ++- .../ThreadsCoordinatorBridgePresenter.swift | 16 ++- .../ThreadsCoordinatorParameters.swift | 5 + ...bleModule.swift => NavigationModule.swift} | 6 +- Riot/Routers/NavigationRouter.swift | 4 +- Riot/Routers/NavigationRouterType.swift | 12 +- Riot/Routers/Presentable.swift | 4 +- 36 files changed, 205 insertions(+), 223 deletions(-) rename Riot/Modules/Room/Views/Threads/{ThreadSummaryViewModel.swift => ThreadSummaryModel.swift} (80%) rename Riot/Modules/Room/Views/Title/Thread/{ThreadRoomTitleViewModel.swift => ThreadRoomTitleModel.swift} (63%) rename Riot/Modules/Threads/ThreadList/Views/Cell/{ThreadViewModel.swift => ThreadModel.swift} (71%) rename Riot/Modules/Threads/ThreadList/Views/Empty/{ThreadListEmptyViewModel.swift => ThreadListEmptyModel.swift} (95%) rename Riot/Routers/{PresentableModule.swift => NavigationModule.swift} (83%) diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 96734f3fb..75e34f0c7 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -434,7 +434,7 @@ Tap the + to start adding people."; "room_join_group_call" = "Join"; "room_no_privileges_to_create_group_call" = "You need to be an admin or a moderator to start a call."; -// MARK: Threads +// MARK: Threads "room_thread_title" = "Thread"; "thread_copy_link_to_thread" = "Copy link to thread"; "threads_title" = "Threads"; diff --git a/Riot/Modules/Application/LegacyAppDelegate.m b/Riot/Modules/Application/LegacyAppDelegate.m index 4125abfa9..54e0ecc52 100644 --- a/Riot/Modules/Application/LegacyAppDelegate.m +++ b/Riot/Modules/Application/LegacyAppDelegate.m @@ -1380,7 +1380,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni RoomNavigationParameters *roomNavigationParameters = [[RoomNavigationParameters alloc] initWithRoomId:roomId eventId:eventId mxSession:account.mxSession - threadParameters:threadParameters + threadParameters:threadParameters presentationParameters:screenPresentationParameters]; [self showRoomWithParameters:roomNavigationParameters]; @@ -2914,7 +2914,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni RoomNavigationParameters *parameters = [[RoomNavigationParameters alloc] initWithRoomId:roomId eventId:eventId mxSession:mxSession - threadParameters:nil + threadParameters:nil presentationParameters:presentationParameters]; [self showRoomWithParameters:parameters]; diff --git a/Riot/Modules/Common/Recents/RecentsViewController.m b/Riot/Modules/Common/Recents/RecentsViewController.m index 93c7883f7..41cfa0b0a 100644 --- a/Riot/Modules/Common/Recents/RecentsViewController.m +++ b/Riot/Modules/Common/Recents/RecentsViewController.m @@ -875,7 +875,7 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro RoomNavigationParameters *parameters = [[RoomNavigationParameters alloc] initWithRoomId:roomId eventId:nil mxSession:matrixSession - threadParameters:nil + threadParameters:nil presentationParameters:presentationParameters]; [[AppDelegate theDelegate] showRoomWithParameters:parameters completion:^{ diff --git a/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m b/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m index 535d48d93..d9274d8be 100644 --- a/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m +++ b/Riot/Modules/GlobalSearch/Files/HomeFilesSearchViewController.m @@ -157,7 +157,7 @@ RoomNavigationParameters *parameters = [[RoomNavigationParameters alloc] initWithRoomId:roomId eventId:eventId mxSession:session - threadParameters:nil + threadParameters:nil presentationParameters:presentationParameters]; [[AppDelegate theDelegate] showRoomWithParameters:parameters]; diff --git a/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m b/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m index b0dd6b913..0ff484676 100644 --- a/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m +++ b/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m @@ -164,7 +164,7 @@ RoomNavigationParameters *parameters = [[RoomNavigationParameters alloc] initWithRoomId:roomId eventId:eventId mxSession:session - threadParameters:nil + threadParameters:nil presentationParameters:presentationParameters]; [[AppDelegate theDelegate] showRoomWithParameters:parameters]; diff --git a/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m b/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m index beeb90385..be2904f17 100644 --- a/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m +++ b/Riot/Modules/GlobalSearch/Rooms/DirectoryViewController.m @@ -248,7 +248,7 @@ RoomNavigationParameters *parameters = [[RoomNavigationParameters alloc] initWithRoomId:roomId eventId:nil mxSession:mxSession - threadParameters:nil + threadParameters:nil presentationParameters:presentationParameters]; [[AppDelegate theDelegate] showRoomWithParameters:parameters]; } diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.h b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.h index 29f727398..6bf9fef65 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.h +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.h @@ -274,7 +274,7 @@ extern NSString *const kMXKRoomDataSourceTimelineErrorErrorKey; @param roomId the id of the room to get data from. @param initialEventId the id of the event where to start the timeline. - @param threadId the id of the thread to load. + @param threadId the id of the thread to load. If provided, thread data source will be loaded from the room specified with `roomId`. @param mxSession the Matrix session to get data from. @param onComplete a block providing the newly created instance. */ @@ -316,7 +316,7 @@ extern NSString *const kMXKRoomDataSourceTimelineErrorErrorKey; @param roomId the id of the room to get data from. @param initialEventId the id of the event where to start the timeline. - @param threadId the id of the thread to initialize. + @param threadId the id of the thread to initialize. If provided, thread data source will be initialized from the room specified with `roomId`. @param mxSession the Matrix session to get data from. @return the newly created instance. */ diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m index 8ecf065bd..a46421ca2 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m @@ -1458,7 +1458,6 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { paginationRequest = [_timeline paginate:numItems direction:direction onlyFromStore:onlyFromStore -// threadId:_threadId complete:^{ MXStrongifyAndReturnIfNil(self); @@ -1526,7 +1525,6 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { secondaryPaginationRequest = [_secondaryTimeline paginate:numItems direction:direction onlyFromStore:onlyFromStore -// threadId:_threadId complete:^{ MXStrongifyAndReturnIfNil(self); diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.h b/Riot/Modules/Room/DataSources/RoomDataSource.h index ecb3c1cf0..eb3a3fd10 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.h +++ b/Riot/Modules/Room/DataSources/RoomDataSource.h @@ -113,14 +113,14 @@ @protocol RoomDataSourceDelegate /** - Called when the room's encryption trust level did updated. + Called when the room's encryption trust level did update. @param roomDataSource room data source instance */ - (void)roomDataSourceDidUpdateEncryptionTrustLevel:(RoomDataSource * _Nonnull)roomDataSource; /** - Called when a thread summary view + Called when a thread summary view is tapped. @param roomDataSource room data source instance */ diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m index 98a94b364..759afbf36 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.m +++ b/Riot/Modules/Room/DataSources/RoomDataSource.m @@ -464,6 +464,7 @@ const CGFloat kTypingCellHeight = 24; { threadSummaryView = [[ThreadSummaryView alloc] initWithThread:component.thread]; threadSummaryView.delegate = self; + threadSummaryView.tag = index; [temporaryViews addObject:threadSummaryView]; UIView *upperDecorationView = reactionsView ?: urlPreviewView; diff --git a/Riot/Modules/Room/RoomCoordinator.swift b/Riot/Modules/Room/RoomCoordinator.swift index 3c7fca9eb..fcf6d287a 100644 --- a/Riot/Modules/Room/RoomCoordinator.swift +++ b/Riot/Modules/Room/RoomCoordinator.swift @@ -134,7 +134,7 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { self.selectedEventId = eventId if self.hasStartedOnce { - self.roomViewController.highlightEvent(eventId, completion: completion) + self.roomViewController.highlightAndDisplayEvent(eventId, completion: completion) } else { self.start(withCompletion: completion) } diff --git a/Riot/Modules/Room/RoomViewController.h b/Riot/Modules/Room/RoomViewController.h index 49adefb8f..c47948655 100644 --- a/Riot/Modules/Room/RoomViewController.h +++ b/Riot/Modules/Room/RoomViewController.h @@ -106,7 +106,7 @@ extern NSNotificationName const RoomGroupCallTileTappedNotification; @param eventId Identifier of the event to be highlighted. @param completion Completion block to be called at the end of process. Optional. */ -- (void)highlightEvent:(NSString *)eventId completion:(nullable void (^)(void))completion; +- (void)highlightAndDisplayEvent:(NSString *)eventId completion:(nullable void (^)(void))completion; /** Creates and returns a new `RoomViewController` object. diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 9324e689c..4fceaf479 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -93,7 +93,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; @interface RoomViewController () + RoomDataSourceDelegate, RoomCreationModalCoordinatorBridgePresenterDelegate, RoomInfoCoordinatorBridgePresenterDelegate, DialpadViewControllerDelegate, RemoveJitsiWidgetViewDelegate, VoiceMessageControllerDelegate, SpaceDetailPresenterDelegate, UserSuggestionCoordinatorBridgeDelegate, ThreadsCoordinatorBridgePresenterDelegate> { // The preview header @@ -197,9 +197,8 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; @property (nonatomic, strong) RoomMessageURLParser *roomMessageURLParser; @property (nonatomic, strong) RoomCreationModalCoordinatorBridgePresenter *roomCreationModalCoordinatorBridgePresenter; @property (nonatomic, strong) RoomInfoCoordinatorBridgePresenter *roomInfoCoordinatorBridgePresenter; -@property (nonatomic, strong) RoomCoordinatorBridgePresenter *threadBridgePresenter; @property (nonatomic, strong) CustomSizedPresentationController *customSizedPresentationController; -@property (nonatomic, strong) ThreadsCoordinatorBridgePresenter *threadsCoordinatorBridgePresenter; +@property (nonatomic, strong) ThreadsCoordinatorBridgePresenter *threadsBridgePresenter; @property (nonatomic, getter=isActivitiesViewExpanded) BOOL activitiesViewExpanded; @property (nonatomic, getter=isScrollToBottomHidden) BOOL scrollToBottomHidden; @property (nonatomic, getter=isMissedDiscussionsBadgeHidden) BOOL missedDiscussionsBadgeHidden; @@ -4341,10 +4340,11 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; - (IBAction)onThreadListTapped:(id)sender { - self.threadsCoordinatorBridgePresenter = [[ThreadsCoordinatorBridgePresenter alloc] initWithSession:self.mainSession - roomId:self.roomDataSource.roomId]; - self.threadsCoordinatorBridgePresenter.delegate = self; - [self.threadsCoordinatorBridgePresenter pushFrom:self.navigationController animated:YES]; + self.threadsBridgePresenter = [[ThreadsCoordinatorBridgePresenter alloc] initWithSession:self.mainSession + roomId:self.roomDataSource.roomId + threadId:nil]; + self.threadsBridgePresenter.delegate = self; + [self.threadsBridgePresenter pushFrom:self.navigationController animated:YES]; } - (IBAction)onIntegrationsPressed:(id)sender @@ -4473,7 +4473,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [super scrollViewWillBeginDragging:scrollView]; } - // if data source is highlighting an event, dismiss the highlight when user dragges the table view + // if data source is highlighting an event, dismiss the highlight when user drags the table view if (customizedRoomDataSource.highlightedEventId) { NSInteger row = [self.roomDataSource indexOfCellDataWithEventId:customizedRoomDataSource.highlightedEventId]; @@ -6352,25 +6352,20 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; - (void)openThreadWithId:(NSString *)threadId { - if (self.threadBridgePresenter) + if (self.threadsBridgePresenter) { - [self.threadBridgePresenter dismissWithAnimated:YES completion:nil]; - self.threadBridgePresenter = nil; + [self.threadsBridgePresenter dismissWithAnimated:YES completion:nil]; + self.threadsBridgePresenter = nil; } - - RoomDisplayConfiguration *configuration = RoomDisplayConfiguration.forThreads; - RoomCoordinatorBridgePresenterParameters *parameters = [[RoomCoordinatorBridgePresenterParameters alloc] initWithSession:self.mainSession - roomId:self.roomDataSource.roomId - eventId:nil - threadId:threadId - displayConfiguration:configuration - previewData:nil]; - self.threadBridgePresenter = [[RoomCoordinatorBridgePresenter alloc] initWithParameters:parameters]; - self.threadBridgePresenter.delegate = self; - [self.threadBridgePresenter pushFrom:self.navigationController animated:YES]; + + self.threadsBridgePresenter = [[ThreadsCoordinatorBridgePresenter alloc] initWithSession:self.mainSession + roomId:self.roomDataSource.roomId + threadId:threadId]; + self.threadsBridgePresenter.delegate = self; + [self.threadsBridgePresenter pushFrom:self.navigationController animated:YES]; } -- (void)highlightEvent:(NSString *)eventId completion:(void (^)(void))completion +- (void)highlightAndDisplayEvent:(NSString *)eventId completion:(void (^)(void))completion { NSInteger row = [self.roomDataSource indexOfCellDataWithEventId:eventId]; if (row == NSNotFound) @@ -6816,66 +6811,29 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [self mention:member]; } -#pragma mark - RoomCoordinatorBridgePresenterDelegate - -- (void)roomCoordinatorBridgePresenterDidLeaveRoom:(RoomCoordinatorBridgePresenter *)bridgePresenter -{ - -} - -- (void)roomCoordinatorBridgePresenterDidCancelRoomPreview:(RoomCoordinatorBridgePresenter *)bridgePresenter -{ - -} - -- (void)roomCoordinatorBridgePresenter:(RoomCoordinatorBridgePresenter *)bridgePresenter - didSelectRoomWithId:(NSString *)roomId - eventId:(NSString*)eventId -{ - if (bridgePresenter == self.threadBridgePresenter && [roomId isEqualToString:self.roomDataSource.roomId] && eventId) - { - // thread view wants to highlight an event in the timeline - // dismiss thread view first - MXWeakify(self); - [self.threadBridgePresenter dismissWithAnimated:YES completion:^{ - MXStrongifyAndReturnIfNil(self); - - [self highlightEvent:eventId completion:nil]; - }]; - } -} - -- (void)roomCoordinatorBridgePresenterDidDismissInteractively:(RoomCoordinatorBridgePresenter *)bridgePresenter -{ - if (bridgePresenter == self.threadBridgePresenter) - { - self.threadBridgePresenter = nil; - } -} - #pragma mark - ThreadsCoordinatorBridgePresenterDelegate - (void)threadsCoordinatorBridgePresenterDelegateDidComplete:(ThreadsCoordinatorBridgePresenter *)coordinatorBridgePresenter { - self.threadsCoordinatorBridgePresenter = nil; + self.threadsBridgePresenter = nil; } - (void)threadsCoordinatorBridgePresenterDelegateDidSelect:(ThreadsCoordinatorBridgePresenter *)coordinatorBridgePresenter roomId:(NSString *)roomId eventId:(NSString *)eventId { MXWeakify(self); - [self.threadsCoordinatorBridgePresenter dismissWithAnimated:YES completion:^{ + [self.threadsBridgePresenter dismissWithAnimated:YES completion:^{ MXStrongifyAndReturnIfNil(self); if (eventId) { - [self highlightEvent:eventId completion:nil]; + [self highlightAndDisplayEvent:eventId completion:nil]; } }]; } - (void)threadsCoordinatorBridgePresenterDidDismissInteractively:(ThreadsCoordinatorBridgePresenter *)coordinatorBridgePresenter { - self.threadsCoordinatorBridgePresenter = nil; + self.threadsBridgePresenter = nil; } @end diff --git a/Riot/Modules/Room/Views/Threads/ThreadSummaryViewModel.swift b/Riot/Modules/Room/Views/Threads/ThreadSummaryModel.swift similarity index 80% rename from Riot/Modules/Room/Views/Threads/ThreadSummaryViewModel.swift rename to Riot/Modules/Room/Views/Threads/ThreadSummaryModel.swift index 11b68dda5..585fc0203 100644 --- a/Riot/Modules/Room/Views/Threads/ThreadSummaryViewModel.swift +++ b/Riot/Modules/Room/Views/Threads/ThreadSummaryModel.swift @@ -16,8 +16,8 @@ import Foundation -struct ThreadSummaryViewModel { - var numberOfReplies: Int - var lastMessageSenderAvatar: AvatarViewDataProtocol? - var lastMessageText: String? +struct ThreadSummaryModel { + let numberOfReplies: Int + let lastMessageSenderAvatar: AvatarViewDataProtocol? + let lastMessageText: String? } diff --git a/Riot/Modules/Room/Views/Threads/ThreadSummaryView.swift b/Riot/Modules/Room/Views/Threads/ThreadSummaryView.swift index 7aa13f872..c24a861c3 100644 --- a/Riot/Modules/Room/Views/Threads/ThreadSummaryView.swift +++ b/Riot/Modules/Room/Views/Threads/ThreadSummaryView.swift @@ -37,7 +37,7 @@ class ThreadSummaryView: UIView { @IBOutlet private weak var lastMessageAvatarView: UserAvatarView! @IBOutlet private weak var lastMessageContentLabel: UILabel! - private(set) var thread: MXThread! + private(set) var thread: MXThread? private lazy var tapGestureRecognizer: UITapGestureRecognizer = { return UITapGestureRecognizer(target: self, action: #selector(tapped(_:))) @@ -57,7 +57,7 @@ class ThreadSummaryView: UIView { configure() } - static func contentViewHeight(forThread thread: MXThread, fitting maxWidth: CGFloat) -> CGFloat { + static func contentViewHeight(forThread thread: MXThread?, fitting maxWidth: CGFloat) -> CGFloat { return Constants.viewHeight } @@ -66,7 +66,7 @@ class ThreadSummaryView: UIView { loadNibContent() } - @nonobjc func configure(withViewModel viewModel: ThreadSummaryViewModel) { + @nonobjc func configure(withViewModel viewModel: ThreadSummaryModel) { numberOfRepliesLabel.text = String(viewModel.numberOfReplies) if let avatar = viewModel.lastMessageSenderAvatar { lastMessageAvatarView.fill(with: avatar) @@ -105,7 +105,7 @@ class ThreadSummaryView: UIView { let formatterError = UnsafeMutablePointer.allocate(capacity: 1) let lastMessageText = eventFormatter.string(from: lastMessage, with: roomState, error: formatterError) - let viewModel = ThreadSummaryViewModel(numberOfReplies: thread.numberOfReplies, + let viewModel = ThreadSummaryModel(numberOfReplies: thread.numberOfReplies, lastMessageSenderAvatar: avatarViewData, lastMessageText: lastMessageText) self.configure(withViewModel: viewModel) @@ -121,8 +121,6 @@ class ThreadSummaryView: UIView { } } -// extension ThreadSummaryView: NibLoadable {} - extension ThreadSummaryView: NibOwnerLoadable {} extension ThreadSummaryView: Themable { diff --git a/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleViewModel.swift b/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleModel.swift similarity index 63% rename from Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleViewModel.swift rename to Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleModel.swift index 9db319dea..059d99f5e 100644 --- a/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleViewModel.swift +++ b/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleModel.swift @@ -16,12 +16,12 @@ import Foundation -struct ThreadRoomTitleViewModel { - var roomAvatar: AvatarViewDataProtocol? - var roomEncryptionBadge: UIImage? - var roomDisplayName: String? +struct ThreadRoomTitleModel { + let roomAvatar: AvatarViewDataProtocol? + let roomEncryptionBadge: UIImage? + let roomDisplayName: String? - static let empty = ThreadRoomTitleViewModel(roomAvatar: nil, - roomEncryptionBadge: nil, - roomDisplayName: nil) + static let empty = ThreadRoomTitleModel(roomAvatar: nil, + roomEncryptionBadge: nil, + roomDisplayName: nil) } diff --git a/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.swift b/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.swift index a39d39a8e..05ac863b8 100644 --- a/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.swift +++ b/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.swift @@ -44,7 +44,7 @@ class ThreadRoomTitleView: RoomTitleView { // MARK: - Methods - func configure(withViewModel viewModel: ThreadRoomTitleViewModel) { + func configure(withViewModel viewModel: ThreadRoomTitleModel) { if let avatarViewData = viewModel.roomAvatar { roomAvatarView.fill(with: avatarViewData) } else { @@ -87,9 +87,9 @@ class ThreadRoomTitleView: RoomTitleView { encrpytionBadge = nil } - let viewModel = ThreadRoomTitleViewModel(roomAvatar: avatarViewData, - roomEncryptionBadge: encrpytionBadge, - roomDisplayName: room.displayName) + let viewModel = ThreadRoomTitleModel(roomAvatar: avatarViewData, + roomEncryptionBadge: encrpytionBadge, + roomDisplayName: room.displayName) configure(withViewModel: viewModel) } diff --git a/Riot/Modules/SplitView/SplitViewCoordinator.swift b/Riot/Modules/SplitView/SplitViewCoordinator.swift index bd56a095a..442da486e 100644 --- a/Riot/Modules/SplitView/SplitViewCoordinator.swift +++ b/Riot/Modules/SplitView/SplitViewCoordinator.swift @@ -363,13 +363,13 @@ extension SplitViewCoordinator: SplitViewMasterPresentableDelegate { detailNavigationRouter.push(detailPresentable, animated: true, popCompletion: popCompletion) } - func splitViewMasterPresentable(_ presentable: Presentable, wantsToReplaceDetailsWith modules: [PresentableModule]) { + func splitViewMasterPresentable(_ presentable: Presentable, wantsToReplaceDetailsWith modules: [NavigationModule]) { MXLog.debug("[SplitViewCoordinator] splitViewMasterPresentable: \(presentable) wantsToReplaceDetailsWith modules: \(modules)") self.detailNavigationRouter?.setModules(modules, animated: true) } - func splitViewMasterPresentable(_ presentable: Presentable, wantsToStack modules: [PresentableModule]) { + func splitViewMasterPresentable(_ presentable: Presentable, wantsToStack modules: [NavigationModule]) { guard let detailNavigationRouter = self.detailNavigationRouter else { MXLog.warning("[SplitViewCoordinator] Failed to stack \(modules) because detailNavigationRouter is nil") return diff --git a/Riot/Modules/SplitView/SplitViewPresentable.swift b/Riot/Modules/SplitView/SplitViewPresentable.swift index 7dcb0da12..4e63663f1 100644 --- a/Riot/Modules/SplitView/SplitViewPresentable.swift +++ b/Riot/Modules/SplitView/SplitViewPresentable.swift @@ -28,10 +28,10 @@ protocol SplitViewMasterPresentableDelegate: AnyObject { func splitViewMasterPresentable(_ presentable: Presentable, wantsToStack detailPresentable: Presentable, popCompletion: (() -> Void)?) /// Replace split view detail with the given modules - func splitViewMasterPresentable(_ presentable: Presentable, wantsToReplaceDetailsWith modules: [PresentableModule]) + func splitViewMasterPresentable(_ presentable: Presentable, wantsToReplaceDetailsWith modules: [NavigationModule]) /// Stack modules on the existing split view detail stack - func splitViewMasterPresentable(_ presentable: Presentable, wantsToStack modules: [PresentableModule]) + func splitViewMasterPresentable(_ presentable: Presentable, wantsToStack modules: [NavigationModule]) /// Pop to module on the existing split view detail stack func splitViewMasterPresentable(_ presentable: Presentable, wantsToPopTo module: Presentable) diff --git a/Riot/Modules/TabBar/TabBarCoordinator.swift b/Riot/Modules/TabBar/TabBarCoordinator.swift index 141e9ff5b..f9d27589d 100644 --- a/Riot/Modules/TabBar/TabBarCoordinator.swift +++ b/Riot/Modules/TabBar/TabBarCoordinator.swift @@ -395,57 +395,8 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { private func showRoom(withNavigationParameters roomNavigationParameters: RoomNavigationParameters, completion: (() -> Void)?) { if let threadParameters = roomNavigationParameters.threadParameters, threadParameters.stackRoomScreen { - self.activityIndicatorPresenter.presentActivityIndicator(on: toPresentable().view, animated: false) - let dispatchGroup = DispatchGroup() - - // create room coordinator - let roomCoordinatorParameters = RoomCoordinatorParameters(navigationRouterStore: NavigationRouterStore.shared, - session: roomNavigationParameters.mxSession, - roomId: roomNavigationParameters.roomId, - eventId: nil, - threadId: nil) - - dispatchGroup.enter() - let roomCoordinator = RoomCoordinator(parameters: roomCoordinatorParameters) - roomCoordinator.delegate = self - roomCoordinator.start { - dispatchGroup.leave() - } - self.add(childCoordinator: roomCoordinator) - - // create thread coordinator - let threadCoordinatorParameters = RoomCoordinatorParameters(navigationRouterStore: NavigationRouterStore.shared, - session: roomNavigationParameters.mxSession, - roomId: roomNavigationParameters.roomId, - eventId: roomNavigationParameters.eventId, - threadId: roomNavigationParameters.threadParameters?.threadId) - - dispatchGroup.enter() - let threadCoordinator = RoomCoordinator(parameters: threadCoordinatorParameters) - threadCoordinator.delegate = self - threadCoordinator.start { - dispatchGroup.leave() - } - self.add(childCoordinator: threadCoordinator) - - dispatchGroup.notify(queue: .main) { [weak self] in - guard let self = self else { return } - let modules: [PresentableModule] = [ - PresentableModule(presentable: roomCoordinator, popCompletion: { [weak self] in - // NOTE: The RoomDataSource releasing is handled in SplitViewCoordinator - self?.remove(childCoordinator: roomCoordinator) - }), - PresentableModule(presentable: threadCoordinator, popCompletion: { [weak self] in - // NOTE: The RoomDataSource releasing is handled in SplitViewCoordinator - self?.remove(childCoordinator: threadCoordinator) - }) - ] - - self.showSplitViewDetails(with: modules, - stack: roomNavigationParameters.presentationParameters.stackAboveVisibleViews) - - self.activityIndicatorPresenter.removeCurrentActivityIndicator(animated: true) - } + showRoomAndThread(with: roomNavigationParameters, + completion: completion) } else { let roomCoordinatorParameters = RoomCoordinatorParameters(navigationRouterStore: NavigationRouterStore.shared, session: roomNavigationParameters.mxSession, @@ -522,6 +473,63 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { self?.remove(childCoordinator: coordinator) } } + + private func showRoomAndThread(with roomNavigationParameters: RoomNavigationParameters, + completion: (() -> Void)? = nil) { + self.activityIndicatorPresenter.presentActivityIndicator(on: toPresentable().view, animated: false) + let dispatchGroup = DispatchGroup() + + // create room coordinator + let roomCoordinatorParameters = RoomCoordinatorParameters(navigationRouterStore: NavigationRouterStore.shared, + session: roomNavigationParameters.mxSession, + roomId: roomNavigationParameters.roomId, + eventId: nil, + threadId: nil) + + dispatchGroup.enter() + let roomCoordinator = RoomCoordinator(parameters: roomCoordinatorParameters) + roomCoordinator.delegate = self + roomCoordinator.start { + dispatchGroup.leave() + } + self.add(childCoordinator: roomCoordinator) + + // create thread coordinator + let threadCoordinatorParameters = RoomCoordinatorParameters(navigationRouterStore: NavigationRouterStore.shared, + session: roomNavigationParameters.mxSession, + roomId: roomNavigationParameters.roomId, + eventId: roomNavigationParameters.eventId, + threadId: roomNavigationParameters.threadParameters?.threadId) + + dispatchGroup.enter() + let threadCoordinator = RoomCoordinator(parameters: threadCoordinatorParameters) + threadCoordinator.delegate = self + threadCoordinator.start { + dispatchGroup.leave() + } + self.add(childCoordinator: threadCoordinator) + + dispatchGroup.notify(queue: .main) { [weak self] in + guard let self = self else { return } + let modules: [NavigationModule] = [ + NavigationModule(presentable: roomCoordinator, popCompletion: { [weak self] in + // NOTE: The RoomDataSource releasing is handled in SplitViewCoordinator + self?.remove(childCoordinator: roomCoordinator) + }), + NavigationModule(presentable: threadCoordinator, popCompletion: { [weak self] in + // NOTE: The RoomDataSource releasing is handled in SplitViewCoordinator + self?.remove(childCoordinator: threadCoordinator) + }) + ] + + self.showSplitViewDetails(with: modules, + stack: roomNavigationParameters.presentationParameters.stackAboveVisibleViews) + + self.activityIndicatorPresenter.removeCurrentActivityIndicator(animated: true) + + completion?() + } + } // MARK: Split view @@ -544,7 +552,7 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType { } } - private func showSplitViewDetails(with modules: [PresentableModule], stack: Bool) { + private func showSplitViewDetails(with modules: [NavigationModule], stack: Bool) { if stack { self.splitViewMasterPresentableDelegate?.splitViewMasterPresentable(self, wantsToStack: modules) } else { diff --git a/Riot/Modules/Threads/ThreadList/ThreadListCoordinatorProtocol.swift b/Riot/Modules/Threads/ThreadList/ThreadListCoordinatorProtocol.swift index a4227b909..d88e50581 100644 --- a/Riot/Modules/Threads/ThreadList/ThreadListCoordinatorProtocol.swift +++ b/Riot/Modules/Threads/ThreadList/ThreadListCoordinatorProtocol.swift @@ -24,7 +24,7 @@ protocol ThreadListCoordinatorDelegate: AnyObject { func threadListCoordinatorDidCancel(_ coordinator: ThreadListCoordinatorProtocol) } -/// `ThreadListCoordinatorProtocol` is a protocol describing a Coordinator that handle xxxxxxx navigation flow. +/// `ThreadListCoordinatorProtocol` is a protocol describing a Coordinator that handle thread list navigation flow. protocol ThreadListCoordinatorProtocol: Coordinator, Presentable { var delegate: ThreadListCoordinatorDelegate? { get } } diff --git a/Riot/Modules/Threads/ThreadList/ThreadListViewController.swift b/Riot/Modules/Threads/ThreadList/ThreadListViewController.swift index 56f16c5df..319c0f774 100644 --- a/Riot/Modules/Threads/ThreadList/ThreadListViewController.swift +++ b/Riot/Modules/Threads/ThreadList/ThreadListViewController.swift @@ -20,12 +20,6 @@ import UIKit final class ThreadListViewController: UIViewController { - // MARK: - Constants - - private enum Constants { - static let aConstant: Int = 666 - } - // MARK: - Properties // MARK: Outlets @@ -172,7 +166,7 @@ final class ThreadListViewController: UIViewController { navigationItem.rightBarButtonItem?.isEnabled = true } - private func renderEmptyView(withViewModel emptyViewModel: ThreadListEmptyViewModel) { + private func renderEmptyView(withViewModel emptyViewModel: ThreadListEmptyModel) { self.activityPresenter.removeCurrentActivityIndicator(animated: true) emptyView.configure(withViewModel: emptyViewModel) threadsTableView.isHidden = true diff --git a/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift b/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift index 1e980520d..0a04ff8cf 100644 --- a/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift +++ b/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift @@ -83,14 +83,14 @@ final class ThreadListViewModel: ThreadListViewModelProtocol { return threads.count } - func threadViewModel(at index: Int) -> ThreadViewModel? { + func threadViewModel(at index: Int) -> ThreadModel? { guard index < threads.count else { return nil } return viewModel(forThread: threads[index]) } - var titleViewModel: ThreadRoomTitleViewModel { + var titleViewModel: ThreadRoomTitleModel { guard let room = session.room(withRoomId: roomId) else { return .empty } @@ -109,33 +109,33 @@ final class ThreadListViewModel: ThreadListViewModelProtocol { encrpytionBadge = nil } - return ThreadRoomTitleViewModel(roomAvatar: avatarViewData, - roomEncryptionBadge: encrpytionBadge, - roomDisplayName: room.displayName) + return ThreadRoomTitleModel(roomAvatar: avatarViewData, + roomEncryptionBadge: encrpytionBadge, + roomDisplayName: room.displayName) } - private var emptyViewModel: ThreadListEmptyViewModel { + private var emptyViewModel: ThreadListEmptyModel { switch selectedFilterType { case .all: - return ThreadListEmptyViewModel(icon: Asset.Images.roomContextMenuReplyInThread.image, - title: VectorL10n.threadsEmptyTitle, - info: VectorL10n.threadsEmptyInfoAll, - tip: VectorL10n.threadsEmptyTip, - showAllThreadsButtonTitle: VectorL10n.threadsEmptyShowAllThreads, - showAllThreadsButtonHidden: true) + return ThreadListEmptyModel(icon: Asset.Images.roomContextMenuReplyInThread.image, + title: VectorL10n.threadsEmptyTitle, + info: VectorL10n.threadsEmptyInfoAll, + tip: VectorL10n.threadsEmptyTip, + showAllThreadsButtonTitle: VectorL10n.threadsEmptyShowAllThreads, + showAllThreadsButtonHidden: true) case .myThreads: - return ThreadListEmptyViewModel(icon: Asset.Images.roomContextMenuReplyInThread.image, - title: VectorL10n.threadsEmptyTitle, - info: VectorL10n.threadsEmptyInfoMy, - tip: VectorL10n.threadsEmptyTip, - showAllThreadsButtonTitle: VectorL10n.threadsEmptyShowAllThreads, - showAllThreadsButtonHidden: false) + return ThreadListEmptyModel(icon: Asset.Images.roomContextMenuReplyInThread.image, + title: VectorL10n.threadsEmptyTitle, + info: VectorL10n.threadsEmptyInfoMy, + tip: VectorL10n.threadsEmptyTip, + showAllThreadsButtonTitle: VectorL10n.threadsEmptyShowAllThreads, + showAllThreadsButtonHidden: false) } } // MARK: - Private - private func viewModel(forThread thread: MXThread) -> ThreadViewModel { + private func viewModel(forThread thread: MXThread) -> ThreadModel { let rootAvatarViewData: AvatarViewData? let rootMessageSender: MXUser? let lastAvatarViewData: AvatarViewData? @@ -175,15 +175,15 @@ final class ThreadListViewModel: ThreadListViewModelProtocol { lastMessageSender = nil } - let summaryViewModel = ThreadSummaryViewModel(numberOfReplies: thread.numberOfReplies, - lastMessageSenderAvatar: lastAvatarViewData, - lastMessageText: lastMessageText) + let summaryViewModel = ThreadSummaryModel(numberOfReplies: thread.numberOfReplies, + lastMessageSenderAvatar: lastAvatarViewData, + lastMessageText: lastMessageText) - return ThreadViewModel(rootMessageSenderAvatar: rootAvatarViewData, - rootMessageSenderDisplayName: rootMessageSender?.displayname, - rootMessageText: rootMessageText, - lastMessageTime: lastMessageTime, - summaryViewModel: summaryViewModel) + return ThreadModel(rootMessageSenderAvatar: rootAvatarViewData, + rootMessageSenderDisplayName: rootMessageSender?.displayname, + rootMessageText: rootMessageText, + lastMessageTime: lastMessageTime, + summaryViewModel: summaryViewModel) } private func rootMessageText(forThread thread: MXThread) -> String? { diff --git a/Riot/Modules/Threads/ThreadList/ThreadListViewModelProtocol.swift b/Riot/Modules/Threads/ThreadList/ThreadListViewModelProtocol.swift index be0892c47..d54dc3bca 100644 --- a/Riot/Modules/Threads/ThreadList/ThreadListViewModelProtocol.swift +++ b/Riot/Modules/Threads/ThreadList/ThreadListViewModelProtocol.swift @@ -38,10 +38,10 @@ protocol ThreadListViewModelProtocol { var viewState: ThreadListViewState { get } - var titleViewModel: ThreadRoomTitleViewModel { get } + var titleViewModel: ThreadRoomTitleModel { get } var selectedFilterType: ThreadListFilterType { get } var numberOfThreads: Int { get } - func threadViewModel(at index: Int) -> ThreadViewModel? + func threadViewModel(at index: Int) -> ThreadModel? } enum ThreadListFilterType { diff --git a/Riot/Modules/Threads/ThreadList/ThreadListViewState.swift b/Riot/Modules/Threads/ThreadList/ThreadListViewState.swift index 073c70645..0bd236788 100644 --- a/Riot/Modules/Threads/ThreadList/ThreadListViewState.swift +++ b/Riot/Modules/Threads/ThreadList/ThreadListViewState.swift @@ -23,7 +23,7 @@ enum ThreadListViewState { case idle case loading case loaded - case empty(_ viewModel: ThreadListEmptyViewModel) + case empty(_ viewModel: ThreadListEmptyModel) case showingFilterTypes case error(Error) } diff --git a/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadViewModel.swift b/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadModel.swift similarity index 71% rename from Riot/Modules/Threads/ThreadList/Views/Cell/ThreadViewModel.swift rename to Riot/Modules/Threads/ThreadList/Views/Cell/ThreadModel.swift index 9bc842345..e27bda614 100644 --- a/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadViewModel.swift +++ b/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadModel.swift @@ -16,10 +16,10 @@ import Foundation -struct ThreadViewModel { - var rootMessageSenderAvatar: AvatarViewDataProtocol? - var rootMessageSenderDisplayName: String? - var rootMessageText: String? - var lastMessageTime: String? - var summaryViewModel: ThreadSummaryViewModel? +struct ThreadModel { + let rootMessageSenderAvatar: AvatarViewDataProtocol? + let rootMessageSenderDisplayName: String? + let rootMessageText: String? + let lastMessageTime: String? + let summaryViewModel: ThreadSummaryModel? } diff --git a/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.swift b/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.swift index 000ee1b57..da0ad2403 100644 --- a/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.swift +++ b/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.swift @@ -35,7 +35,7 @@ class ThreadTableViewCell: UITableViewCell { separatorInset = Constants.separatorInset } - func configure(withViewModel viewModel: ThreadViewModel) { + func configure(withViewModel viewModel: ThreadModel) { if let rootAvatar = viewModel.rootMessageSenderAvatar { rootMessageAvatarView.fill(with: rootAvatar) } else { diff --git a/Riot/Modules/Threads/ThreadList/Views/Empty/ThreadListEmptyViewModel.swift b/Riot/Modules/Threads/ThreadList/Views/Empty/ThreadListEmptyModel.swift similarity index 95% rename from Riot/Modules/Threads/ThreadList/Views/Empty/ThreadListEmptyViewModel.swift rename to Riot/Modules/Threads/ThreadList/Views/Empty/ThreadListEmptyModel.swift index bef214bba..74162b2a0 100644 --- a/Riot/Modules/Threads/ThreadList/Views/Empty/ThreadListEmptyViewModel.swift +++ b/Riot/Modules/Threads/ThreadList/Views/Empty/ThreadListEmptyModel.swift @@ -16,7 +16,7 @@ import Foundation -struct ThreadListEmptyViewModel { +struct ThreadListEmptyModel { let icon: UIImage? let title: String? let info: String? diff --git a/Riot/Modules/Threads/ThreadList/Views/Empty/ThreadListEmptyView.swift b/Riot/Modules/Threads/ThreadList/Views/Empty/ThreadListEmptyView.swift index 213f1fa18..ffe973d1a 100644 --- a/Riot/Modules/Threads/ThreadList/Views/Empty/ThreadListEmptyView.swift +++ b/Riot/Modules/Threads/ThreadList/Views/Empty/ThreadListEmptyView.swift @@ -22,6 +22,7 @@ protocol ThreadListEmptyViewDelegate: AnyObject { func threadListEmptyViewTappedShowAllThreads(_ emptyView: ThreadListEmptyView) } +/// View to be shown on the thread list screen when no thread is available. Use a `ThreadListEmptyModel` instance to configure. class ThreadListEmptyView: UIView { @IBOutlet weak var delegate: ThreadListEmptyViewDelegate? @@ -38,7 +39,7 @@ class ThreadListEmptyView: UIView { loadNibContent() } - func configure(withViewModel viewModel: ThreadListEmptyViewModel) { + func configure(withViewModel viewModel: ThreadListEmptyModel) { iconView.image = viewModel.icon titleLabel.text = viewModel.title infoLabel.text = viewModel.info diff --git a/Riot/Modules/Threads/ThreadsCoordinator.swift b/Riot/Modules/Threads/ThreadsCoordinator.swift index 807d6d125..05de0c434 100644 --- a/Riot/Modules/Threads/ThreadsCoordinator.swift +++ b/Riot/Modules/Threads/ThreadsCoordinator.swift @@ -54,7 +54,12 @@ final class ThreadsCoordinator: NSObject, ThreadsCoordinatorProtocol { func start() { - let rootCoordinator = self.createThreadListCoordinator() + let rootCoordinator: Coordinator & Presentable + if let threadId = parameters.threadId { + rootCoordinator = createThreadCoordinator(forThreadId: threadId) + } else { + rootCoordinator = createThreadListCoordinator() + } rootCoordinator.start() @@ -77,6 +82,10 @@ final class ThreadsCoordinator: NSObject, ThreadsCoordinatorProtocol { func stop() { if selectedThreadCoordinator != nil { let modules = self.navigationRouter.modules + // if a thread is selected from the thread list coordinator, then navigation stack will look like: + // ... -> Screen A -> Thread List Screen -> Thread Screen + // we'll try to pop to Screen A here + // sanity check: navigation stack contains at least 3 items guard modules.count >= 3 else { return } @@ -116,13 +125,13 @@ final class ThreadsCoordinator: NSObject, ThreadsCoordinatorProtocol { return coordinator } - private func createThreadCoordinator(forThread thread: MXThread) -> RoomCoordinator { + private func createThreadCoordinator(forThreadId threadId: String) -> RoomCoordinator { let parameters = RoomCoordinatorParameters(navigationRouter: navigationRouter, navigationRouterStore: nil, session: parameters.session, roomId: parameters.roomId, eventId: nil, - threadId: thread.id, + threadId: threadId, displayConfiguration: .forThreads) let coordinator = RoomCoordinator(parameters: parameters) coordinator.delegate = self @@ -145,7 +154,7 @@ extension ThreadsCoordinator: ThreadListCoordinatorDelegate { } func threadListCoordinatorDidSelectThread(_ coordinator: ThreadListCoordinatorProtocol, thread: MXThread) { - let roomCoordinator = createThreadCoordinator(forThread: thread) + let roomCoordinator = createThreadCoordinator(forThreadId: thread.id) selectedThreadCoordinator = roomCoordinator roomCoordinator.start() self.add(childCoordinator: roomCoordinator) diff --git a/Riot/Modules/Threads/ThreadsCoordinatorBridgePresenter.swift b/Riot/Modules/Threads/ThreadsCoordinatorBridgePresenter.swift index 6c12eb40c..43879b292 100644 --- a/Riot/Modules/Threads/ThreadsCoordinatorBridgePresenter.swift +++ b/Riot/Modules/Threads/ThreadsCoordinatorBridgePresenter.swift @@ -45,6 +45,7 @@ final class ThreadsCoordinatorBridgePresenter: NSObject { private let session: MXSession private let roomId: String + private let threadId: String? private var navigationType: NavigationType = .present private var coordinator: ThreadsCoordinator? @@ -53,11 +54,18 @@ final class ThreadsCoordinatorBridgePresenter: NSObject { weak var delegate: ThreadsCoordinatorBridgePresenterDelegate? // MARK: - Setup - + + /// Initializer + /// - Parameters: + /// - session: Session instance + /// - roomId: Room identifier + /// - threadId: Thread identifier. Specified thread will be opened if provided, the thread list otherwise init(session: MXSession, - roomId: String) { + roomId: String, + threadId: String?) { self.session = session self.roomId = roomId + self.threadId = threadId super.init() } @@ -71,7 +79,8 @@ final class ThreadsCoordinatorBridgePresenter: NSObject { func present(from viewController: UIViewController, animated: Bool) { let threadsCoordinatorParameters = ThreadsCoordinatorParameters(session: self.session, - roomId: self.roomId) + roomId: self.roomId, + threadId: self.threadId) let threadsCoordinator = ThreadsCoordinator(parameters: threadsCoordinatorParameters) threadsCoordinator.delegate = self @@ -89,6 +98,7 @@ final class ThreadsCoordinatorBridgePresenter: NSObject { let threadsCoordinatorParameters = ThreadsCoordinatorParameters(session: self.session, roomId: self.roomId, + threadId: self.threadId, navigationRouter: navigationRouter) let threadsCoordinator = ThreadsCoordinator(parameters: threadsCoordinatorParameters) diff --git a/Riot/Modules/Threads/ThreadsCoordinatorParameters.swift b/Riot/Modules/Threads/ThreadsCoordinatorParameters.swift index 3e21ad86e..83a2d47be 100644 --- a/Riot/Modules/Threads/ThreadsCoordinatorParameters.swift +++ b/Riot/Modules/Threads/ThreadsCoordinatorParameters.swift @@ -26,15 +26,20 @@ struct ThreadsCoordinatorParameters { /// Room identifier let roomId: String + + /// Thread identifier. Specified thread will be opened if provided, the thread list otherwise + let threadId: String? /// The navigation router that manage physical navigation let navigationRouter: NavigationRouterType init(session: MXSession, roomId: String, + threadId: String?, navigationRouter: NavigationRouterType? = nil) { self.session = session self.roomId = roomId + self.threadId = threadId self.navigationRouter = navigationRouter ?? NavigationRouter(navigationController: RiotNavigationController()) } } diff --git a/Riot/Routers/PresentableModule.swift b/Riot/Routers/NavigationModule.swift similarity index 83% rename from Riot/Routers/PresentableModule.swift rename to Riot/Routers/NavigationModule.swift index 4ee7e3379..01c54bb98 100644 --- a/Riot/Routers/PresentableModule.swift +++ b/Riot/Routers/NavigationModule.swift @@ -17,7 +17,7 @@ import Foundation /// Structure used to pass modules to routers with pop completion blocks. -struct PresentableModule { +struct NavigationModule { /// Actual presentable of the module let presentable: Presentable @@ -27,10 +27,10 @@ struct PresentableModule { // MARK: - CustomStringConvertible -extension PresentableModule: CustomStringConvertible { +extension NavigationModule: CustomStringConvertible { var description: String { - return "PresentableModule: \(presentable), pop completion: \(String(describing: popCompletion))" + return "NavigationModule: \(presentable), pop completion: \(String(describing: popCompletion))" } } diff --git a/Riot/Routers/NavigationRouter.swift b/Riot/Routers/NavigationRouter.swift index c406cc7e7..23c05aff1 100755 --- a/Riot/Routers/NavigationRouter.swift +++ b/Riot/Routers/NavigationRouter.swift @@ -117,7 +117,7 @@ final class NavigationRouter: NSObject, NavigationRouterType { self.didPushViewController(controller) } - func setModules(_ modules: [PresentableModule], hideNavigationBar: Bool, animated: Bool) { + func setModules(_ modules: [NavigationModule], hideNavigationBar: Bool, animated: Bool) { MXLog.debug("[NavigationRouter] Set modules \(modules)") @@ -221,7 +221,7 @@ final class NavigationRouter: NSObject, NavigationRouterType { self.didPushViewController(controller) } - func push(_ modules: [PresentableModule], animated: Bool) { + func push(_ modules: [NavigationModule], animated: Bool) { MXLog.debug("[NavigationRouter] Push modules \(modules)") // Avoid pushing any UINavigationController onto stack diff --git a/Riot/Routers/NavigationRouterType.swift b/Riot/Routers/NavigationRouterType.swift index ca248526f..f1275efa0 100755 --- a/Riot/Routers/NavigationRouterType.swift +++ b/Riot/Routers/NavigationRouterType.swift @@ -45,7 +45,7 @@ protocol NavigationRouterType: AnyObject, Presentable { /// - modules: The modules stack to set. /// - hideNavigationBar: Specify true to hide the UINavigationBar. /// - animated: Specify true to animate the transition. - func setModules(_ modules: [PresentableModule], hideNavigationBar: Bool, animated: Bool) + func setModules(_ modules: [NavigationModule], hideNavigationBar: Bool, animated: Bool) /// Pop to root view controller of navigation controller and remove all others /// @@ -68,7 +68,7 @@ protocol NavigationRouterType: AnyObject, Presentable { /// /// - Parameter modules: Modules to push /// - Parameter animated: Specify true to animate the transition. - func push(_ modules: [PresentableModule], animated: Bool) + func push(_ modules: [NavigationModule], animated: Bool) /// Pop last view controller from navigation controller stack /// @@ -99,7 +99,7 @@ extension NavigationRouterType { setRootModule(module, hideNavigationBar: false, animated: false, popCompletion: popCompletion) } - func setModules(_ modules: [PresentableModule], animated: Bool) { + func setModules(_ modules: [NavigationModule], animated: Bool) { setModules(modules, hideNavigationBar: false, animated: animated) } @@ -109,15 +109,15 @@ extension NavigationRouterType { } -// MARK: - Presentable <--> ModulePresentable Transitive Methods +// MARK: - Presentable <--> NavigationModule Transitive Methods extension NavigationRouterType { - func setRootModule(_ module: PresentableModule) { + func setRootModule(_ module: NavigationModule) { setRootModule(module.presentable, popCompletion: module.popCompletion) } - func push(_ module: PresentableModule, animated: Bool) { + func push(_ module: NavigationModule, animated: Bool) { push(module.presentable, animated: animated, popCompletion: module.popCompletion) } diff --git a/Riot/Routers/Presentable.swift b/Riot/Routers/Presentable.swift index 15943afbf..e9f760393 100755 --- a/Riot/Routers/Presentable.swift +++ b/Riot/Routers/Presentable.swift @@ -31,8 +31,8 @@ extension Presentable { /// Returns a new module from the presentable without a pop completion block /// - Returns: Module - func toModule() -> PresentableModule { - return PresentableModule(presentable: self, popCompletion: nil) + func toModule() -> NavigationModule { + return NavigationModule(presentable: self, popCompletion: nil) } } From 58675a7a1abbd99bf41326fa8f3d6df0a0d3c700 Mon Sep 17 00:00:00 2001 From: ismailgulek Date: Thu, 27 Jan 2022 01:45:01 +0300 Subject: [PATCH 87/87] Fix further review remarks --- Riot/Generated/Images.swift | 1 + .../Room/Views/Threads/ThreadSummaryView.swift | 16 ++++++++-------- .../Title/Thread/ThreadRoomTitleView.swift | 18 +++++++++--------- .../ThreadList/ThreadListViewController.swift | 14 +++++++------- .../ThreadList/ThreadListViewModel.swift | 16 ++++++++-------- .../ThreadListViewModelProtocol.swift | 4 ++-- .../ThreadList/Views/Cell/ThreadModel.swift | 2 +- .../Views/Cell/ThreadTableViewCell.swift | 14 +++++++------- .../Views/Empty/ThreadListEmptyView.swift | 14 +++++++------- 9 files changed, 50 insertions(+), 49 deletions(-) diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index 30f385366..cf52a37fe 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -136,6 +136,7 @@ internal enum Asset { internal static let roomContextMenuReply = ImageAsset(name: "room_context_menu_reply") internal static let roomContextMenuReplyInThread = ImageAsset(name: "room_context_menu_reply_in_thread") internal static let roomContextMenuRetry = ImageAsset(name: "room_context_menu_retry") + internal static let roomContextMenuThread = ImageAsset(name: "room_context_menu_thread") internal static let inputCloseIcon = ImageAsset(name: "input_close_icon") internal static let inputEditIcon = ImageAsset(name: "input_edit_icon") internal static let inputReplyIcon = ImageAsset(name: "input_reply_icon") diff --git a/Riot/Modules/Room/Views/Threads/ThreadSummaryView.swift b/Riot/Modules/Room/Views/Threads/ThreadSummaryView.swift index c24a861c3..369188e58 100644 --- a/Riot/Modules/Room/Views/Threads/ThreadSummaryView.swift +++ b/Riot/Modules/Room/Views/Threads/ThreadSummaryView.swift @@ -66,14 +66,14 @@ class ThreadSummaryView: UIView { loadNibContent() } - @nonobjc func configure(withViewModel viewModel: ThreadSummaryModel) { - numberOfRepliesLabel.text = String(viewModel.numberOfReplies) - if let avatar = viewModel.lastMessageSenderAvatar { + @nonobjc func configure(withModel model: ThreadSummaryModel) { + numberOfRepliesLabel.text = String(model.numberOfReplies) + if let avatar = model.lastMessageSenderAvatar { lastMessageAvatarView.fill(with: avatar) } else { lastMessageAvatarView.avatarImageView.image = nil } - lastMessageContentLabel.text = viewModel.lastMessageText + lastMessageContentLabel.text = model.lastMessageText } private func configure() { @@ -105,10 +105,10 @@ class ThreadSummaryView: UIView { let formatterError = UnsafeMutablePointer.allocate(capacity: 1) let lastMessageText = eventFormatter.string(from: lastMessage, with: roomState, error: formatterError) - let viewModel = ThreadSummaryModel(numberOfReplies: thread.numberOfReplies, - lastMessageSenderAvatar: avatarViewData, - lastMessageText: lastMessageText) - self.configure(withViewModel: viewModel) + let model = ThreadSummaryModel(numberOfReplies: thread.numberOfReplies, + lastMessageSenderAvatar: avatarViewData, + lastMessageText: lastMessageText) + self.configure(withModel: model) } } diff --git a/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.swift b/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.swift index 05ac863b8..85340578e 100644 --- a/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.swift +++ b/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.swift @@ -44,15 +44,15 @@ class ThreadRoomTitleView: RoomTitleView { // MARK: - Methods - func configure(withViewModel viewModel: ThreadRoomTitleModel) { - if let avatarViewData = viewModel.roomAvatar { + func configure(withModel model: ThreadRoomTitleModel) { + if let avatarViewData = model.roomAvatar { roomAvatarView.fill(with: avatarViewData) } else { roomAvatarView.avatarImageView.image = nil } - roomEncryptionBadgeView.image = viewModel.roomEncryptionBadge - roomEncryptionBadgeView.isHidden = viewModel.roomEncryptionBadge == nil - roomNameLabel.text = viewModel.roomDisplayName + roomEncryptionBadgeView.image = model.roomEncryptionBadge + roomEncryptionBadgeView.isHidden = model.roomEncryptionBadge == nil + roomNameLabel.text = model.roomDisplayName } // MARK: - Overrides @@ -87,10 +87,10 @@ class ThreadRoomTitleView: RoomTitleView { encrpytionBadge = nil } - let viewModel = ThreadRoomTitleModel(roomAvatar: avatarViewData, - roomEncryptionBadge: encrpytionBadge, - roomDisplayName: room.displayName) - configure(withViewModel: viewModel) + let model = ThreadRoomTitleModel(roomAvatar: avatarViewData, + roomEncryptionBadge: encrpytionBadge, + roomDisplayName: room.displayName) + configure(withModel: model) } override func awakeFromNib() { diff --git a/Riot/Modules/Threads/ThreadList/ThreadListViewController.swift b/Riot/Modules/Threads/ThreadList/ThreadListViewController.swift index 319c0f774..9c2a75a9f 100644 --- a/Riot/Modules/Threads/ThreadList/ThreadListViewController.swift +++ b/Riot/Modules/Threads/ThreadList/ThreadListViewController.swift @@ -119,7 +119,7 @@ final class ThreadListViewController: UIViewController { private func setupViews() { let titleView = ThreadRoomTitleView.loadFromNib() titleView.mode = .allThreads - titleView.configure(withViewModel: viewModel.titleViewModel) + titleView.configure(withModel: viewModel.titleModel) titleView.updateLayout(for: UIApplication.shared.statusBarOrientation) self.titleView = titleView navigationItem.leftItemsSupplementBackButton = true @@ -144,8 +144,8 @@ final class ThreadListViewController: UIViewController { self.renderLoading() case .loaded: self.renderLoaded() - case .empty(let viewModel): - self.renderEmptyView(withViewModel: viewModel) + case .empty(let model): + self.renderEmptyView(withModel: model) case .showingFilterTypes: self.renderShowingFilterTypes() case .error(let error): @@ -166,9 +166,9 @@ final class ThreadListViewController: UIViewController { navigationItem.rightBarButtonItem?.isEnabled = true } - private func renderEmptyView(withViewModel emptyViewModel: ThreadListEmptyModel) { + private func renderEmptyView(withModel model: ThreadListEmptyModel) { self.activityPresenter.removeCurrentActivityIndicator(animated: true) - emptyView.configure(withViewModel: emptyViewModel) + emptyView.configure(withModel: model) threadsTableView.isHidden = true emptyView.isHidden = false navigationItem.rightBarButtonItem?.isEnabled = viewModel.selectedFilterType == .myThreads @@ -243,8 +243,8 @@ extension ThreadListViewController: UITableViewDataSource { func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell: ThreadTableViewCell = tableView.dequeueReusableCell(for: indexPath) - if let threadVM = viewModel.threadViewModel(at: indexPath.row) { - cell.configure(withViewModel: threadVM) + if let threadModel = viewModel.threadModel(at: indexPath.row) { + cell.configure(withModel: threadModel) } cell.update(theme: theme) diff --git a/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift b/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift index 0a04ff8cf..77bb98487 100644 --- a/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift +++ b/Riot/Modules/Threads/ThreadList/ThreadListViewModel.swift @@ -83,14 +83,14 @@ final class ThreadListViewModel: ThreadListViewModelProtocol { return threads.count } - func threadViewModel(at index: Int) -> ThreadModel? { + func threadModel(at index: Int) -> ThreadModel? { guard index < threads.count else { return nil } - return viewModel(forThread: threads[index]) + return model(forThread: threads[index]) } - var titleViewModel: ThreadRoomTitleModel { + var titleModel: ThreadRoomTitleModel { guard let room = session.room(withRoomId: roomId) else { return .empty } @@ -135,7 +135,7 @@ final class ThreadListViewModel: ThreadListViewModelProtocol { // MARK: - Private - private func viewModel(forThread thread: MXThread) -> ThreadModel { + private func model(forThread thread: MXThread) -> ThreadModel { let rootAvatarViewData: AvatarViewData? let rootMessageSender: MXUser? let lastAvatarViewData: AvatarViewData? @@ -175,15 +175,15 @@ final class ThreadListViewModel: ThreadListViewModelProtocol { lastMessageSender = nil } - let summaryViewModel = ThreadSummaryModel(numberOfReplies: thread.numberOfReplies, - lastMessageSenderAvatar: lastAvatarViewData, - lastMessageText: lastMessageText) + let summaryModel = ThreadSummaryModel(numberOfReplies: thread.numberOfReplies, + lastMessageSenderAvatar: lastAvatarViewData, + lastMessageText: lastMessageText) return ThreadModel(rootMessageSenderAvatar: rootAvatarViewData, rootMessageSenderDisplayName: rootMessageSender?.displayname, rootMessageText: rootMessageText, lastMessageTime: lastMessageTime, - summaryViewModel: summaryViewModel) + summaryModel: summaryModel) } private func rootMessageText(forThread thread: MXThread) -> String? { diff --git a/Riot/Modules/Threads/ThreadList/ThreadListViewModelProtocol.swift b/Riot/Modules/Threads/ThreadList/ThreadListViewModelProtocol.swift index d54dc3bca..d375e9f3a 100644 --- a/Riot/Modules/Threads/ThreadList/ThreadListViewModelProtocol.swift +++ b/Riot/Modules/Threads/ThreadList/ThreadListViewModelProtocol.swift @@ -38,10 +38,10 @@ protocol ThreadListViewModelProtocol { var viewState: ThreadListViewState { get } - var titleViewModel: ThreadRoomTitleModel { get } + var titleModel: ThreadRoomTitleModel { get } var selectedFilterType: ThreadListFilterType { get } var numberOfThreads: Int { get } - func threadViewModel(at index: Int) -> ThreadModel? + func threadModel(at index: Int) -> ThreadModel? } enum ThreadListFilterType { diff --git a/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadModel.swift b/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadModel.swift index e27bda614..28b68303d 100644 --- a/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadModel.swift +++ b/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadModel.swift @@ -21,5 +21,5 @@ struct ThreadModel { let rootMessageSenderDisplayName: String? let rootMessageText: String? let lastMessageTime: String? - let summaryViewModel: ThreadSummaryModel? + let summaryModel: ThreadSummaryModel? } diff --git a/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.swift b/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.swift index da0ad2403..4dfd8d1f6 100644 --- a/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.swift +++ b/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadTableViewCell.swift @@ -35,17 +35,17 @@ class ThreadTableViewCell: UITableViewCell { separatorInset = Constants.separatorInset } - func configure(withViewModel viewModel: ThreadModel) { - if let rootAvatar = viewModel.rootMessageSenderAvatar { + func configure(withModel model: ThreadModel) { + if let rootAvatar = model.rootMessageSenderAvatar { rootMessageAvatarView.fill(with: rootAvatar) } else { rootMessageAvatarView.avatarImageView.image = nil } - rootMessageSenderLabel.text = viewModel.rootMessageSenderDisplayName - rootMessageContentLabel.text = viewModel.rootMessageText - lastMessageTimeLabel.text = viewModel.lastMessageTime - if let summaryViewModel = viewModel.summaryViewModel { - summaryView.configure(withViewModel: summaryViewModel) + rootMessageSenderLabel.text = model.rootMessageSenderDisplayName + rootMessageContentLabel.text = model.rootMessageText + lastMessageTimeLabel.text = model.lastMessageTime + if let summaryModel = model.summaryModel { + summaryView.configure(withModel: summaryModel) } } diff --git a/Riot/Modules/Threads/ThreadList/Views/Empty/ThreadListEmptyView.swift b/Riot/Modules/Threads/ThreadList/Views/Empty/ThreadListEmptyView.swift index ffe973d1a..523282378 100644 --- a/Riot/Modules/Threads/ThreadList/Views/Empty/ThreadListEmptyView.swift +++ b/Riot/Modules/Threads/ThreadList/Views/Empty/ThreadListEmptyView.swift @@ -39,14 +39,14 @@ class ThreadListEmptyView: UIView { loadNibContent() } - func configure(withViewModel viewModel: ThreadListEmptyModel) { - iconView.image = viewModel.icon - titleLabel.text = viewModel.title - infoLabel.text = viewModel.info - tipLabel.text = viewModel.tip - showAllThreadsButton.setTitle(viewModel.showAllThreadsButtonTitle, + func configure(withModel model: ThreadListEmptyModel) { + iconView.image = model.icon + titleLabel.text = model.title + infoLabel.text = model.info + tipLabel.text = model.tip + showAllThreadsButton.setTitle(model.showAllThreadsButtonTitle, for: .normal) - showAllThreadsButton.isHidden = viewModel.showAllThreadsButtonHidden + showAllThreadsButton.isHidden = model.showAllThreadsButtonHidden titleLabel.isHidden = titleLabel.text?.isEmpty ?? true infoLabel.isHidden = infoLabel.text?.isEmpty ?? true