mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-05-06 16:07:42 +02:00
Merge pull request #5089 from vector-im/ismail/5068_start_thread
Start a new Thread by replying to a message
This commit is contained in:
@@ -64,27 +64,6 @@
|
||||
</objects>
|
||||
<point key="canvasLocation" x="5047" y="-1437"/>
|
||||
</scene>
|
||||
<!--Room Context Timeline-->
|
||||
<scene sceneID="Htr-h8-baq">
|
||||
<objects>
|
||||
<viewController title="Room Context Timeline" hidesBottomBarWhenPushed="YES" id="Too-LV-OLW" customClass="RoomViewController" sceneMemberID="viewController">
|
||||
<navigationItem key="navigationItem" id="yLe-Hk-Sol">
|
||||
<nil key="title"/>
|
||||
<view key="titleView" contentMode="scaleToFill" id="djN-zB-Vni" userLabel="Room title view container">
|
||||
<rect key="frame" x="8" y="2" width="312" height="40"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</view>
|
||||
</navigationItem>
|
||||
<connections>
|
||||
<outlet property="roomTitleViewContainer" destination="djN-zB-Vni" id="VQG-Mp-hSa"/>
|
||||
<segue destination="gkO-rP-nGK" kind="show" identifier="showContactDetails" id="ziz-Xl-QVg"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="Yjg-uP-Hcy" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="3326" y="-1299"/>
|
||||
</scene>
|
||||
<!--Room Search View Controller-->
|
||||
<scene sceneID="rUg-1s-vHX">
|
||||
<objects>
|
||||
@@ -98,9 +77,6 @@
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</view>
|
||||
<connections>
|
||||
<segue destination="Too-LV-OLW" kind="show" identifier="showTimeline" id="P1V-0d-mYL"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="bK5-DX-KSF" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
@@ -543,10 +519,29 @@
|
||||
</objects>
|
||||
<point key="canvasLocation" x="374" y="449"/>
|
||||
</scene>
|
||||
<!--Thread-->
|
||||
<scene sceneID="Opl-gU-pwm">
|
||||
<objects>
|
||||
<viewController storyboardIdentifier="ThreadViewControllerStoryboardId" title="Room" hidesBottomBarWhenPushed="YES" id="R2h-H9-hdJ" userLabel="Thread" customClass="ThreadViewController" customModule="Riot" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<navigationItem key="navigationItem" id="TFF-nx-BSb">
|
||||
<nil key="title"/>
|
||||
<view key="titleView" contentMode="scaleToFill" id="e4J-vI-jzo" userLabel="Room title view container">
|
||||
<rect key="frame" x="8" y="2" width="312" height="40"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</view>
|
||||
</navigationItem>
|
||||
<connections>
|
||||
<outlet property="roomTitleViewContainer" destination="e4J-vI-jzo" id="b1C-TY-2R6"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="kHX-sJ-uYE" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-153" y="-419"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<inferredMetricsTieBreakers>
|
||||
<segue reference="Tfl-tq-LQp"/>
|
||||
<segue reference="f5u-Y1-7nt"/>
|
||||
</inferredMetricsTieBreakers>
|
||||
<resources>
|
||||
<image name="launch_screen_logo" width="240" height="240"/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+23
@@ -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
|
||||
}
|
||||
}
|
||||
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 318 B |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 418 B |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 706 B |
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
+23
@@ -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
|
||||
}
|
||||
}
|
||||
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 204 B |
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 295 B |
Vendored
BIN
Binary file not shown.
|
After Width: | Height: | Size: 377 B |
@@ -338,7 +338,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";
|
||||
@@ -378,7 +379,8 @@ Tap the + to start adding people.";
|
||||
"room_event_action_more" = "More";
|
||||
"room_event_action_share" = "Share";
|
||||
"room_event_action_forward" = "Forward";
|
||||
"room_event_action_permalink" = "Permalink";
|
||||
"room_event_action_view_in_room" = "View in room";
|
||||
"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";
|
||||
@@ -395,6 +397,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" = "Thread";
|
||||
"room_event_action_edit" = "Edit";
|
||||
"room_event_action_reaction_show_all" = "Show all";
|
||||
"room_event_action_reaction_show_less" = "Show less";
|
||||
@@ -423,12 +426,26 @@ 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_accessibility_thread_more" = "More";
|
||||
"room_place_voice_call" = "Voice call";
|
||||
"room_open_dialpad" = "Dial pad";
|
||||
"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";
|
||||
"thread_copy_link_to_thread" = "Copy link to thread";
|
||||
"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";
|
||||
"media_type_accessibility_video" = "Video";
|
||||
@@ -585,6 +602,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_enabled_polls" = "Polls";
|
||||
"settings_labs_enable_threads" = "Threaded messaging";
|
||||
|
||||
"settings_version" = "Version %@";
|
||||
"settings_olm_version" = "Olm Version %@";
|
||||
@@ -919,6 +937,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: %@";
|
||||
|
||||
@@ -134,7 +134,9 @@ internal enum Asset {
|
||||
internal static let roomContextMenuEdit = ImageAsset(name: "room_context_menu_edit")
|
||||
internal static let roomContextMenuMore = ImageAsset(name: "room_context_menu_more")
|
||||
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")
|
||||
@@ -158,6 +160,7 @@ internal enum Asset {
|
||||
internal static let pollTypeCheckboxDefault = ImageAsset(name: "poll_type_checkbox_default")
|
||||
internal static let pollTypeCheckboxSelected = ImageAsset(name: "poll_type_checkbox_selected")
|
||||
internal static let pollWinnerIcon = ImageAsset(name: "poll_winner_icon")
|
||||
internal static let threadsFilter = ImageAsset(name: "threads_filter")
|
||||
internal static let urlPreviewClose = ImageAsset(name: "url_preview_close")
|
||||
internal static let urlPreviewCloseDark = ImageAsset(name: "url_preview_close_dark")
|
||||
internal static let voiceMessageCancelGradient = ImageAsset(name: "voice_message_cancel_gradient")
|
||||
|
||||
@@ -279,6 +279,11 @@ internal enum StoryboardScene {
|
||||
|
||||
internal static let initialScene = InitialSceneType<Riot.TemplateScreenViewController>(storyboard: TemplateScreenViewController.self)
|
||||
}
|
||||
internal enum ThreadListViewController: StoryboardType {
|
||||
internal static let storyboardName = "ThreadListViewController"
|
||||
|
||||
internal static let initialScene = InitialSceneType<Riot.ThreadListViewController>(storyboard: ThreadListViewController.self)
|
||||
}
|
||||
internal enum UserVerificationSessionStatusViewController: StoryboardType {
|
||||
internal static let storyboardName = "UserVerificationSessionStatusViewController"
|
||||
|
||||
|
||||
@@ -1419,6 +1419,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")
|
||||
@@ -2687,6 +2691,14 @@ public class VectorL10n: NSObject {
|
||||
public static var roomAccessibilitySearch: String {
|
||||
return VectorL10n.tr("Vector", "room_accessibility_search")
|
||||
}
|
||||
/// More
|
||||
public static var roomAccessibilityThreadMore: String {
|
||||
return VectorL10n.tr("Vector", "room_accessibility_thread_more")
|
||||
}
|
||||
/// 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")
|
||||
@@ -3167,7 +3179,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")
|
||||
}
|
||||
@@ -3199,6 +3211,10 @@ public class VectorL10n: NSObject {
|
||||
public static var roomEventActionReply: String {
|
||||
return VectorL10n.tr("Vector", "room_event_action_reply")
|
||||
}
|
||||
/// Thread
|
||||
public static var roomEventActionReplyInThread: String {
|
||||
return VectorL10n.tr("Vector", "room_event_action_reply_in_thread")
|
||||
}
|
||||
/// Report content
|
||||
public static var roomEventActionReport: String {
|
||||
return VectorL10n.tr("Vector", "room_event_action_report")
|
||||
@@ -3231,6 +3247,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")
|
||||
@@ -3799,6 +3819,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")
|
||||
@@ -4787,6 +4811,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")
|
||||
}
|
||||
/// Polls
|
||||
public static var settingsLabsEnabledPolls: String {
|
||||
return VectorL10n.tr("Vector", "settings_labs_enabled_polls")
|
||||
@@ -5295,6 +5323,42 @@ 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")
|
||||
}
|
||||
/// My threads
|
||||
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")
|
||||
}
|
||||
/// Favourites
|
||||
public static var titleFavourites: String {
|
||||
return VectorL10n.tr("Vector", "title_favourites")
|
||||
|
||||
@@ -136,6 +136,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: false, storage: defaults)
|
||||
var enableThreads
|
||||
|
||||
// MARK: Calls
|
||||
|
||||
/// Indicate if `allowStunServerFallback` settings has been set once.
|
||||
|
||||
@@ -1281,6 +1281,7 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
|
||||
}
|
||||
|
||||
NSString *roomIdOrAlias;
|
||||
ThreadParameters *threadParameters;
|
||||
NSString *eventId;
|
||||
NSString *userId;
|
||||
NSString *groupId;
|
||||
@@ -1362,7 +1363,25 @@ 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];
|
||||
if (event.threadId)
|
||||
{
|
||||
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
|
||||
mxSession:account.mxSession
|
||||
threadParameters:threadParameters
|
||||
presentationParameters:screenPresentationParameters];
|
||||
|
||||
[self showRoomWithParameters:roomNavigationParameters];
|
||||
}
|
||||
@@ -2893,7 +2912,10 @@ NSString *const AppDelegateUniversalLinkDidChangeNotification = @"AppDelegateUni
|
||||
ScreenPresentationParameters *presentationParameters = [[ScreenPresentationParameters alloc] initWithRestoreInitialDisplay:YES];
|
||||
|
||||
RoomNavigationParameters *parameters = [[RoomNavigationParameters alloc] initWithRoomId:roomId
|
||||
eventId:eventId mxSession:mxSession presentationParameters:presentationParameters];
|
||||
eventId:eventId
|
||||
mxSession:mxSession
|
||||
threadParameters:nil
|
||||
presentationParameters:presentationParameters];
|
||||
|
||||
[self showRoomWithParameters:parameters];
|
||||
}
|
||||
@@ -3415,7 +3437,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.");
|
||||
}];
|
||||
|
||||
|
||||
@@ -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 {
|
||||
@@ -31,6 +49,9 @@ class RoomNavigationParameters: NSObject {
|
||||
/// 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
|
||||
|
||||
@@ -39,10 +60,12 @@ class RoomNavigationParameters: NSObject {
|
||||
init(roomId: String,
|
||||
eventId: String?,
|
||||
mxSession: MXSession,
|
||||
threadParameters: ThreadParameters?,
|
||||
presentationParameters: ScreenPresentationParameters) {
|
||||
self.roomId = roomId
|
||||
self.eventId = eventId
|
||||
self.mxSession = mxSession
|
||||
self.threadParameters = threadParameters
|
||||
self.presentationParameters = presentationParameters
|
||||
|
||||
super.init()
|
||||
|
||||
@@ -34,6 +34,7 @@ class RoomPreviewNavigationParameters: RoomNavigationParameters {
|
||||
super.init(roomId: previewData.roomId,
|
||||
eventId: previewData.eventId,
|
||||
mxSession: previewData.mxSession,
|
||||
threadParameters: nil,
|
||||
presentationParameters: presentationParameters)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -875,6 +875,7 @@ NSString *const RecentsViewControllerDataReadyNotification = @"RecentsViewContro
|
||||
RoomNavigationParameters *parameters = [[RoomNavigationParameters alloc] initWithRoomId:roomId
|
||||
eventId:nil
|
||||
mxSession:matrixSession
|
||||
threadParameters:nil
|
||||
presentationParameters:presentationParameters];
|
||||
|
||||
[[AppDelegate theDelegate] showRoomWithParameters:parameters completion:^{
|
||||
|
||||
@@ -157,6 +157,7 @@
|
||||
RoomNavigationParameters *parameters = [[RoomNavigationParameters alloc] initWithRoomId:roomId
|
||||
eventId:eventId
|
||||
mxSession:session
|
||||
threadParameters:nil
|
||||
presentationParameters:presentationParameters];
|
||||
|
||||
[[AppDelegate theDelegate] showRoomWithParameters:parameters];
|
||||
|
||||
@@ -164,6 +164,7 @@
|
||||
RoomNavigationParameters *parameters = [[RoomNavigationParameters alloc] initWithRoomId:roomId
|
||||
eventId:eventId
|
||||
mxSession:session
|
||||
threadParameters:nil
|
||||
presentationParameters:presentationParameters];
|
||||
|
||||
[[AppDelegate theDelegate] showRoomWithParameters:parameters];
|
||||
|
||||
@@ -248,6 +248,7 @@
|
||||
RoomNavigationParameters *parameters = [[RoomNavigationParameters alloc] initWithRoomId:roomId
|
||||
eventId:nil
|
||||
mxSession:mxSession
|
||||
threadParameters:nil
|
||||
presentationParameters:presentationParameters];
|
||||
[[AppDelegate theDelegate] showRoomWithParameters:parameters];
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -626,6 +626,22 @@
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)hasThreadRoot
|
||||
{
|
||||
@synchronized (bubbleComponents)
|
||||
{
|
||||
for (MXKRoomBubbleComponent *component in bubbleComponents)
|
||||
{
|
||||
if (component.thread)
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (MXKRoomBubbleComponentDisplayFix)displayFix
|
||||
{
|
||||
MXKRoomBubbleComponentDisplayFix displayFix = MXKRoomBubbleComponentDisplayFixNone;
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -18,6 +18,13 @@
|
||||
|
||||
#import "MXEvent+MatrixKit.h"
|
||||
#import "MXKSwiftHeader.h"
|
||||
#import <MatrixSDK/MatrixSDK.h>
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
@@ -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. 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.
|
||||
*/
|
||||
+ (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. 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.
|
||||
*/
|
||||
- (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.
|
||||
@@ -697,6 +711,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
|
||||
|
||||
/**
|
||||
|
||||
@@ -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,10 @@ 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
|
||||
complete:^{
|
||||
|
||||
MXStrongifyAndReturnIfNil(self);
|
||||
|
||||
@@ -1489,7 +1522,10 @@ 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
|
||||
complete:^{
|
||||
|
||||
MXStrongifyAndReturnIfNil(self);
|
||||
|
||||
@@ -1676,11 +1712,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 +1857,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 +1877,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 +1891,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 +1910,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 +1925,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 +1940,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)
|
||||
{
|
||||
@@ -1926,6 +1962,7 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) {
|
||||
[_room sendLocationWithLatitude:latitude
|
||||
longitude:longitude
|
||||
description:description
|
||||
threadId:self.threadId
|
||||
localEcho:&localEchoEvent
|
||||
success:success failure:failure];
|
||||
|
||||
@@ -1942,7 +1979,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)
|
||||
{
|
||||
@@ -1969,7 +2006,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])
|
||||
{
|
||||
@@ -1978,7 +2015,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])
|
||||
{
|
||||
@@ -2021,7 +2058,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])
|
||||
@@ -2032,7 +2069,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;
|
||||
}
|
||||
|
||||
@@ -2072,7 +2109,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])
|
||||
@@ -2108,7 +2145,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
|
||||
@@ -2805,27 +2842,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];
|
||||
|
||||
@@ -325,11 +325,11 @@ static NSString *const kHTMLATagRegexPattern = @"<a href=\"(.*?)\">([^<]*)</a>";
|
||||
|
||||
// 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);
|
||||
|
||||
@@ -1252,7 +1252,7 @@ static NSString *const kHTMLATagRegexPattern = @"<a href=\"(.*?)\">([^<]*)</a>";
|
||||
|
||||
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])
|
||||
@@ -1260,17 +1260,17 @@ static NSString *const kHTMLATagRegexPattern = @"<a href=\"(.*?)\">([^<]*)</a>";
|
||||
isHTML =YES;
|
||||
MXJSONModelSetString(body, event.content[@"formatted_body"]);
|
||||
}
|
||||
else if (eventThreadIdentifier)
|
||||
else if (eventThreadId && !RiotSettings.shared.enableThreads)
|
||||
{
|
||||
isHTML = YES;
|
||||
MXJSONModelSetString(body, event.content[kMXMessageBodyKey]);
|
||||
MXEvent *threadRootEvent = [mxSession.store eventWithEventId:eventThreadIdentifier
|
||||
MXEvent *threadRootEvent = [mxSession.store eventWithEventId:eventThreadId
|
||||
inRoom:event.roomId];
|
||||
|
||||
NSString *threadRootEventContent;
|
||||
MXJSONModelSetString(threadRootEventContent, threadRootEvent.content[kMXMessageBodyKey]);
|
||||
body = [NSString stringWithFormat:@"<mx-reply><blockquote><a href=\"%@\">In reply to</a> <a href=\"%@\">%@</a><br>%@</blockquote></mx-reply>%@",
|
||||
[MXTools permalinkToEvent:eventThreadIdentifier inRoom:event.roomId],
|
||||
[MXTools permalinkToEvent:eventThreadId inRoom:event.roomId],
|
||||
[MXTools permalinkToUserWithUserId:threadRootEvent.sender],
|
||||
threadRootEvent.sender,
|
||||
threadRootEventContent,
|
||||
@@ -1361,7 +1361,7 @@ static NSString *const kHTMLATagRegexPattern = @"<a href=\"(.*?)\">([^<]*)</a>";
|
||||
// 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)
|
||||
@@ -1704,7 +1704,7 @@ static NSString *const kHTMLATagRegexPattern = @"<a href=\"(.*?)\">([^<]*)</a>";
|
||||
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];
|
||||
}
|
||||
@@ -2030,7 +2030,7 @@ static NSString *const kHTMLATagRegexPattern = @"<a href=\"(.*?)\">([^<]*)</a>";
|
||||
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;
|
||||
}
|
||||
@@ -2094,7 +2094,7 @@ static NSString *const kHTMLATagRegexPattern = @"<a href=\"(.*?)\">([^<]*)</a>";
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -294,6 +294,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<MXKRoomBubbleCellDataStoring>)cellData
|
||||
@@ -564,6 +575,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];
|
||||
|
||||
@@ -583,6 +596,7 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat
|
||||
|
||||
height+= [self urlPreviewHeightForEventId:eventId];
|
||||
height+= [self reactionHeightForEventId:eventId];
|
||||
height+= [self threadSummaryViewHeightForEventId:eventId];
|
||||
height+= [self readReceiptHeightForEventId:eventId];
|
||||
}
|
||||
|
||||
@@ -621,6 +635,32 @@ 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;
|
||||
}
|
||||
return RoomBubbleCellLayout.threadSummaryViewTopMargin + [ThreadSummaryView contentViewHeightForThread:component.thread fitting:self.maxTextViewWidth];
|
||||
}
|
||||
|
||||
- (CGFloat)urlPreviewHeightForEventId:(NSString*)eventId
|
||||
{
|
||||
MXKRoomBubbleComponent *component = [self bubbleComponentWithLinkForEventId:eventId];
|
||||
|
||||
@@ -19,6 +19,7 @@ import Foundation
|
||||
@objc enum RoomContextualMenuAction: Int {
|
||||
case copy
|
||||
case reply
|
||||
case replyInThread
|
||||
case edit
|
||||
case more
|
||||
case resend
|
||||
@@ -34,6 +35,8 @@ import Foundation
|
||||
title = VectorL10n.roomEventActionCopy
|
||||
case .reply:
|
||||
title = VectorL10n.roomEventActionReply
|
||||
case .replyInThread:
|
||||
title = VectorL10n.roomEventActionReplyInThread
|
||||
case .edit:
|
||||
title = VectorL10n.roomEventActionEdit
|
||||
case .more:
|
||||
@@ -55,6 +58,8 @@ import Foundation
|
||||
image = Asset.Images.roomContextMenuCopy.image
|
||||
case .reply:
|
||||
image = Asset.Images.roomContextMenuReply.image
|
||||
case .replyInThread:
|
||||
image = Asset.Images.roomContextMenuReplyInThread.image
|
||||
case .edit:
|
||||
image = Asset.Images.roomContextMenuEdit.image
|
||||
case .more:
|
||||
|
||||
@@ -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.
|
||||
@@ -55,12 +55,18 @@
|
||||
*/
|
||||
@property(nonatomic, nullable) NSArray<TypingUserInfo *> *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.
|
||||
|
||||
@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 +80,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 +91,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 +102,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 +112,19 @@
|
||||
|
||||
@protocol RoomDataSourceDelegate <MXKDataSourceDelegate>
|
||||
|
||||
- (void)roomDataSource:(RoomDataSource*)roomDataSource didUpdateEncryptionTrustLevel:(RoomEncryptionTrustLevel)roomEncryptionTrustLevel;
|
||||
|
||||
- (void)roomDataSource:(RoomDataSource*)roomDataSource didCancel:(MXEvent *)event;
|
||||
/**
|
||||
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 is tapped.
|
||||
|
||||
@param roomDataSource room data source instance
|
||||
*/
|
||||
- (void)roomDataSource:(RoomDataSource * _Nonnull)roomDataSource
|
||||
didTapThread:(MXThread * _Nonnull)thread;
|
||||
|
||||
@end
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
const CGFloat kTypingCellHeight = 24;
|
||||
|
||||
@interface RoomDataSource() <BubbleReactionsViewModelDelegate, URLPreviewViewDelegate>
|
||||
@interface RoomDataSource() <BubbleReactionsViewModelDelegate, URLPreviewViewDelegate, ThreadSummaryViewDelegate>
|
||||
{
|
||||
// Observe kThemeServiceDidChangeThemeNotification to handle user interface theme change.
|
||||
id kThemeServiceDidChangeThemeNotificationObserver;
|
||||
@@ -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];
|
||||
@@ -231,7 +236,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
|
||||
@@ -239,6 +244,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.threadId 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.threadId isEqualToString:self.threadId])
|
||||
{
|
||||
// ignore the event
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (RiotSettings.shared.enableThreads)
|
||||
{
|
||||
// if not in a thread, ignore all threaded events
|
||||
if (event.threadId)
|
||||
{
|
||||
// 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.threadId)
|
||||
{
|
||||
// ignore the event
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [super shouldQueueEventForProcessing:event roomState:roomState direction:direction];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
|
||||
@@ -342,7 +393,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.
|
||||
@@ -379,7 +430,6 @@ const CGFloat kTypingCellHeight = 24;
|
||||
urlPreviewView.tag = index;
|
||||
|
||||
[temporaryViews addObject:urlPreviewView];
|
||||
|
||||
[cellDecorator addURLPreviewView:urlPreviewView
|
||||
toCell:bubbleCell cellData:cellData contentViewPositionY:bottomPositionY];
|
||||
}
|
||||
@@ -403,11 +453,29 @@ const CGFloat kTypingCellHeight = 24;
|
||||
bubbleReactionsViewModel.viewModelDelegate = self;
|
||||
|
||||
[temporaryViews addObject:reactionsView];
|
||||
|
||||
[cellDecorator addReactionView:reactionsView toCell:bubbleCell
|
||||
cellData:cellData contentViewPositionY:bottomPositionY upperDecorationView:urlPreviewView];
|
||||
}
|
||||
|
||||
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 alloc] initWithThread:component.thread];
|
||||
threadSummaryView.delegate = self;
|
||||
threadSummaryView.tag = index;
|
||||
|
||||
[temporaryViews addObject:threadSummaryView];
|
||||
UIView *upperDecorationView = reactionsView ?: urlPreviewView;
|
||||
|
||||
[cellDecorator addThreadSummaryView:threadSummaryView
|
||||
toCell:bubbleCell
|
||||
cellData:cellData
|
||||
contentViewPositionY:bottomPositionY
|
||||
upperDecorationView:upperDecorationView];
|
||||
}
|
||||
|
||||
MXKReceiptSendersContainer* avatarsContainer;
|
||||
|
||||
// Handle read receipts (if any)
|
||||
@@ -463,8 +531,7 @@ const CGFloat kTypingCellHeight = 24;
|
||||
avatarsContainer.accessibilityIdentifier = @"readReceiptsContainer";
|
||||
|
||||
[temporaryViews addObject:avatarsContainer];
|
||||
|
||||
UIView *upperDecorationView = reactionsView ?: urlPreviewView;
|
||||
UIView *upperDecorationView = threadSummaryView ?: (reactionsView ?: urlPreviewView);
|
||||
|
||||
[cellDecorator addReadReceiptsView:avatarsContainer
|
||||
toCell:bubbleCell
|
||||
@@ -563,14 +630,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];
|
||||
@@ -819,9 +887,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];
|
||||
@@ -924,6 +992,21 @@ const CGFloat kTypingCellHeight = 24;
|
||||
cell.attachmentView.accessibilityLabel = nil;
|
||||
}
|
||||
|
||||
#pragma mark - Threads
|
||||
|
||||
- (void)newThreadCreated:(NSNotification *)notification
|
||||
{
|
||||
NSUInteger count = 0;
|
||||
@synchronized (bubbles)
|
||||
{
|
||||
count = bubbles.count;
|
||||
}
|
||||
if (count > 0)
|
||||
{
|
||||
[self reload];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - BubbleReactionsViewModelDelegate
|
||||
|
||||
- (void)bubbleReactionsViewModel:(BubbleReactionsViewModel *)viewModel didAddReaction:(MXReactionCount *)reactionCount forEventId:(NSString *)eventId
|
||||
@@ -1139,4 +1222,12 @@ const CGFloat kTypingCellHeight = 24;
|
||||
[self refreshCells];
|
||||
}
|
||||
|
||||
#pragma mark - ThreadSummaryViewDelegate
|
||||
|
||||
- (void)threadSummaryViewTapped:(ThreadSummaryView *)summaryView
|
||||
{
|
||||
[self.roomDataSourceDelegate roomDataSource:self
|
||||
didTapThread:summaryView.thread];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
if #available(iOS 14, *) {
|
||||
@@ -103,8 +108,13 @@ 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,
|
||||
eventId: self.parameters.eventId,
|
||||
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)
|
||||
}
|
||||
@@ -124,7 +134,7 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol {
|
||||
self.selectedEventId = eventId
|
||||
|
||||
if self.hasStartedOnce {
|
||||
self.loadRoom(withId: self.parameters.roomId, and: eventId, completion: completion)
|
||||
self.roomViewController.highlightAndDisplayEvent(eventId, completion: completion)
|
||||
} else {
|
||||
self.start(withCompletion: completion)
|
||||
}
|
||||
@@ -160,7 +170,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)
|
||||
@@ -168,6 +178,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 {
|
||||
@@ -190,13 +201,45 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
// 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
|
||||
threadDataSource.highlightedEventId = eventId
|
||||
self.roomViewController.displayRoom(threadDataSource)
|
||||
|
||||
// Give the data source ownership to the room view controller.
|
||||
self.roomViewController.hasRoomDataSourceOwnership = true
|
||||
|
||||
completion?()
|
||||
}
|
||||
}
|
||||
|
||||
private func loadRoomPreview(withData previewData: RoomPreviewData, completion: (() -> Void)?) {
|
||||
|
||||
self.roomViewController.displayRoomPreview(previewData)
|
||||
|
||||
completion?()
|
||||
}
|
||||
|
||||
|
||||
private func startLocationCoordinatorWithEvent(_ event: MXEvent? = nil, bubbleData: MXKRoomBubbleCellDataStoring? = nil) {
|
||||
guard #available(iOS 14.0, *) else {
|
||||
return
|
||||
@@ -278,6 +321,10 @@ extension RoomCoordinator: RoomIdentifiable {
|
||||
return self.parameters.roomId
|
||||
}
|
||||
|
||||
var threadId: String? {
|
||||
return self.parameters.threadId
|
||||
}
|
||||
|
||||
var mxSession: MXSession? {
|
||||
self.parameters.session
|
||||
}
|
||||
@@ -295,8 +342,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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -34,16 +36,26 @@ 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?
|
||||
|
||||
/// Display configuration for the room
|
||||
let displayConfiguration: RoomDisplayConfiguration
|
||||
|
||||
/// The data for the room preview.
|
||||
let previewData: RoomPreviewData?
|
||||
|
||||
init(session: MXSession,
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -60,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
|
||||
|
||||
@@ -75,7 +93,6 @@ final class RoomCoordinatorBridgePresenter: NSObject {
|
||||
// MARK: - Public
|
||||
|
||||
func present(from viewController: UIViewController, animated: Bool) {
|
||||
|
||||
let coordinator = self.createRoomCoordinator()
|
||||
coordinator.delegate = self
|
||||
let presentable = coordinator.toPresentable()
|
||||
@@ -84,6 +101,7 @@ final class RoomCoordinatorBridgePresenter: NSObject {
|
||||
coordinator.start()
|
||||
|
||||
self.coordinator = coordinator
|
||||
self.navigationType = .present
|
||||
}
|
||||
|
||||
func push(from navigationController: UINavigationController, animated: Bool) {
|
||||
@@ -95,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().navigationController else {
|
||||
return
|
||||
}
|
||||
navigationController.popViewController(animated: animated)
|
||||
self.coordinator = nil
|
||||
|
||||
completion?()
|
||||
@@ -117,7 +147,12 @@ 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,
|
||||
displayConfiguration: self.bridgeParameters.displayConfiguration)
|
||||
}
|
||||
|
||||
return RoomCoordinator(parameters: coordinatorParameters)
|
||||
@@ -127,8 +162,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) {
|
||||
|
||||
@@ -37,6 +37,12 @@ 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?
|
||||
|
||||
/// Display configuration for the room
|
||||
let displayConfiguration: RoomDisplayConfiguration
|
||||
|
||||
/// The data for the room preview.
|
||||
let previewData: RoomPreviewData?
|
||||
|
||||
@@ -47,12 +53,16 @@ struct RoomCoordinatorParameters {
|
||||
session: MXSession,
|
||||
roomId: String,
|
||||
eventId: String?,
|
||||
threadId: String?,
|
||||
displayConfiguration: RoomDisplayConfiguration,
|
||||
previewData: RoomPreviewData?) {
|
||||
self.navigationRouter = navigationRouter
|
||||
self.navigationRouterStore = navigationRouterStore
|
||||
self.session = session
|
||||
self.roomId = roomId
|
||||
self.eventId = eventId
|
||||
self.threadId = threadId
|
||||
self.displayConfiguration = displayConfiguration
|
||||
self.previewData = previewData
|
||||
}
|
||||
|
||||
@@ -61,9 +71,18 @@ struct RoomCoordinatorParameters {
|
||||
navigationRouterStore: NavigationRouterStoreProtocol? = nil,
|
||||
session: MXSession,
|
||||
roomId: String,
|
||||
eventId: String? = nil) {
|
||||
eventId: String? = nil,
|
||||
threadId: String? = nil,
|
||||
displayConfiguration: RoomDisplayConfiguration = .default) {
|
||||
|
||||
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,
|
||||
displayConfiguration: displayConfiguration,
|
||||
previewData: nil)
|
||||
}
|
||||
|
||||
/// Init to present a room preview
|
||||
@@ -71,6 +90,13 @@ 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,
|
||||
displayConfiguration: .default,
|
||||
previewData: previewData)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -94,12 +100,22 @@ 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)highlightAndDisplayEvent:(NSString *)eventId completion:(nullable void (^)(void))completion;
|
||||
|
||||
/**
|
||||
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
|
||||
|
||||
@@ -129,9 +145,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.
|
||||
|
||||
@@ -93,7 +93,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
@interface RoomViewController () <UISearchBarDelegate, UIGestureRecognizerDelegate, UIScrollViewAccessibilityDelegate, RoomTitleViewTapGestureDelegate, RoomParticipantsViewControllerDelegate, MXKRoomMemberDetailsViewControllerDelegate, ContactsTableViewControllerDelegate, MXServerNoticesDelegate, RoomContextualMenuViewControllerDelegate,
|
||||
ReactionsMenuViewModelCoordinatorDelegate, EditHistoryCoordinatorBridgePresenterDelegate, MXKDocumentPickerPresenterDelegate, EmojiPickerCoordinatorBridgePresenterDelegate,
|
||||
ReactionHistoryCoordinatorBridgePresenterDelegate, CameraPresenterDelegate, MediaPickerCoordinatorBridgePresenterDelegate,
|
||||
RoomDataSourceDelegate, RoomCreationModalCoordinatorBridgePresenterDelegate, RoomInfoCoordinatorBridgePresenterDelegate, DialpadViewControllerDelegate, RemoveJitsiWidgetViewDelegate, VoiceMessageControllerDelegate, SpaceDetailPresenterDelegate, UserSuggestionCoordinatorBridgeDelegate>
|
||||
RoomDataSourceDelegate, RoomCreationModalCoordinatorBridgePresenterDelegate, RoomInfoCoordinatorBridgePresenterDelegate, DialpadViewControllerDelegate, RemoveJitsiWidgetViewDelegate, VoiceMessageControllerDelegate, SpaceDetailPresenterDelegate, UserSuggestionCoordinatorBridgeDelegate, ThreadsCoordinatorBridgePresenterDelegate>
|
||||
{
|
||||
|
||||
// The preview header
|
||||
@@ -198,6 +198,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
@property (nonatomic, strong) RoomCreationModalCoordinatorBridgePresenter *roomCreationModalCoordinatorBridgePresenter;
|
||||
@property (nonatomic, strong) RoomInfoCoordinatorBridgePresenter *roomInfoCoordinatorBridgePresenter;
|
||||
@property (nonatomic, strong) CustomSizedPresentationController *customSizedPresentationController;
|
||||
@property (nonatomic, strong) ThreadsCoordinatorBridgePresenter *threadsBridgePresenter;
|
||||
@property (nonatomic, getter=isActivitiesViewExpanded) BOOL activitiesViewExpanded;
|
||||
@property (nonatomic, getter=isScrollToBottomHidden) BOOL scrollToBottomHidden;
|
||||
@property (nonatomic, getter=isMissedDiscussionsBadgeHidden) BOOL missedDiscussionsBadgeHidden;
|
||||
@@ -210,6 +211,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
@property (nonatomic, strong) UserSuggestionCoordinatorBridge *userSuggestionCoordinator;
|
||||
@property (nonatomic, weak) IBOutlet UIView *userSuggestionContainerView;
|
||||
|
||||
@property (nonatomic, readwrite) RoomDisplayConfiguration *displayConfiguration;
|
||||
@property (nonatomic) AnalyticsScreenTimer *screenTimer;
|
||||
|
||||
@end
|
||||
@@ -227,14 +229,29 @@ 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"];
|
||||
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 -
|
||||
@@ -1386,8 +1403,34 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
return item;
|
||||
}
|
||||
|
||||
- (UIBarButtonItem *)threadMoreBarButtonItem
|
||||
{
|
||||
UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@"room_context_menu_more"]
|
||||
style:UIBarButtonItemStylePlain
|
||||
target:self
|
||||
action:@selector(onButtonPressed:)];
|
||||
item.accessibilityLabel = [VectorL10n roomAccessibilityThreadMore];
|
||||
|
||||
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)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
self.removeJitsiWidgetView = [RemoveJitsiWidgetView instantiate];
|
||||
self.removeJitsiWidgetView.delegate = self;
|
||||
|
||||
@@ -1430,6 +1473,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;
|
||||
}
|
||||
@@ -1593,12 +1640,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
|
||||
@@ -1608,6 +1655,22 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
}
|
||||
|
||||
[self refreshMissedDiscussionsCount:YES];
|
||||
|
||||
if (RiotSettings.shared.enableThreads)
|
||||
{
|
||||
if (self.roomDataSource.threadId)
|
||||
{
|
||||
// in a thread
|
||||
UIBarButtonItem *itemThreadMore = [self threadMoreBarButtonItem];
|
||||
[rightBarButtonItems insertObject:itemThreadMore atIndex:0];
|
||||
}
|
||||
else
|
||||
{
|
||||
// in a regular timeline
|
||||
UIBarButtonItem *itemThreadList = [self threadListBarButtonItem];
|
||||
[rightBarButtonItems insertObject:itemThreadList atIndex:0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.navigationItem.rightBarButtonItems = rightBarButtonItems;
|
||||
@@ -2067,7 +2130,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
{
|
||||
if (self.delegate)
|
||||
{
|
||||
[self.delegate roomViewController:self showRoomWithId:roomId];
|
||||
[self.delegate roomViewController:self showRoomWithId:roomId eventId:nil];
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -3109,6 +3172,22 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
MXWeakify(self);
|
||||
UIAlertController *actionsMenu = [UIAlertController alertControllerWithTitle:nil message:nil preferredStyle:UIAlertControllerStyleActionSheet];
|
||||
|
||||
BOOL showThreadOption = RiotSettings.shared.enableThreads
|
||||
&& !self.roomDataSource.threadId
|
||||
&& !selectedEvent.threadId;
|
||||
if (showThreadOption && [self canCopyEvent:selectedEvent andCell:cell])
|
||||
{
|
||||
[actionsMenu 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)
|
||||
{
|
||||
@@ -3124,7 +3203,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
}]];
|
||||
|
||||
[actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionDelete]
|
||||
style:UIAlertActionStyleDefault
|
||||
style:UIAlertActionStyleDestructive
|
||||
handler:^(UIAlertAction * action) {
|
||||
MXStrongifyAndReturnIfNil(self);
|
||||
|
||||
@@ -3170,6 +3249,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
|
||||
[actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionViewInRoom]
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:^(UIAlertAction * action) {
|
||||
MXStrongifyAndReturnIfNil(self);
|
||||
[self.delegate roomViewController:self
|
||||
showRoomWithId:self.roomDataSource.roomId
|
||||
eventId:selectedEvent.eventId];
|
||||
}]];
|
||||
}
|
||||
|
||||
if (selectedEvent.sentState == MXEventSentStateSent &&
|
||||
selectedEvent.eventType != MXEventTypePollStart &&
|
||||
!selectedEvent.location)
|
||||
@@ -3378,7 +3471,7 @@ 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)
|
||||
@@ -3417,7 +3510,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
}]];
|
||||
}
|
||||
|
||||
if (selectedEvent.eventType == MXEventTypePollStart && [selectedEvent.sender isEqualToString:self.mainSession.myUser.userId]) {
|
||||
if (selectedEvent.eventType == MXEventTypePollStart && [selectedEvent.sender isEqualToString:self.mainSession.myUserId]) {
|
||||
if ([self.delegate roomViewController:self canEndPollWithEventIdentifier:selectedEvent.eventId]) {
|
||||
[actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionEndPoll]
|
||||
style:UIAlertActionStyleDefault
|
||||
@@ -3507,7 +3600,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
}
|
||||
}
|
||||
|
||||
if (![selectedEvent.sender isEqualToString:self.mainSession.myUser.userId] && RiotSettings.shared.roomContextualMenuShowReportContentOption)
|
||||
if (![selectedEvent.sender isEqualToString:self.mainSession.myUserId] && RiotSettings.shared.roomContextualMenuShowReportContentOption)
|
||||
{
|
||||
[actionsMenu addAction:[UIAlertAction actionWithTitle:[VectorL10n roomEventActionReport]
|
||||
style:UIAlertActionStyleDefault
|
||||
@@ -3872,6 +3965,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
|
||||
customizedRoomDataSource.showBubbleDateTimeOnSelection = YES;
|
||||
customizedRoomDataSource.selectedEventId = nil;
|
||||
customizedRoomDataSource.highlightedEventId = nil;
|
||||
|
||||
[self restoreTextMessageBeforeEditing];
|
||||
|
||||
@@ -3926,12 +4020,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.id];
|
||||
}
|
||||
|
||||
#pragma mark - Segues
|
||||
|
||||
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
|
||||
@@ -4239,6 +4338,15 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
[self placeCallWithVideo:YES];
|
||||
}
|
||||
|
||||
- (IBAction)onThreadListTapped:(id)sender
|
||||
{
|
||||
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
|
||||
{
|
||||
WidgetPickerViewController *widgetPicker = [[WidgetPickerViewController alloc] initForMXSession:self.roomDataSource.mxSession
|
||||
@@ -4261,7 +4369,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];
|
||||
@@ -4360,6 +4472,25 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
{
|
||||
[super scrollViewWillBeginDragging:scrollView];
|
||||
}
|
||||
|
||||
// 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];
|
||||
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
|
||||
@@ -4484,7 +4615,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];
|
||||
@@ -4766,6 +4901,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) {
|
||||
@@ -4802,6 +4942,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;
|
||||
@@ -5434,6 +5579,11 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
|
||||
- (void)refreshRemoveJitsiWidgetView
|
||||
{
|
||||
if (!self.displayConfiguration.jitsiWidgetRemoverEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.roomDataSource.isLive && !self.roomDataSource.isPeeking)
|
||||
{
|
||||
Widget *jitsiWidget = [customizedRoomDataSource jitsiWidget];
|
||||
@@ -5814,25 +5964,29 @@ 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);
|
||||
BOOL showThreadOption = RiotSettings.shared.enableThreads && !self.roomDataSource.threadId && !event.threadId;
|
||||
|
||||
NSMutableArray<RoomContextualMenuItem*> *items = [NSMutableArray arrayWithCapacity:5];
|
||||
|
||||
if (!showThreadOption)
|
||||
{
|
||||
[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]];
|
||||
if (showMoreOption)
|
||||
{
|
||||
return @[
|
||||
[self copyMenuItemWithEvent:event andCell:cell],
|
||||
[self replyMenuItemWithEvent:event],
|
||||
[self editMenuItemWithEvent:event],
|
||||
[self moreMenuItemWithEvent:event andCell:cell]
|
||||
];
|
||||
}
|
||||
else
|
||||
{
|
||||
return @[
|
||||
[self copyMenuItemWithEvent:event andCell:cell],
|
||||
[self replyMenuItemWithEvent:event],
|
||||
[self editMenuItemWithEvent:event]
|
||||
];
|
||||
[items addObject:[self moreMenuItemWithEvent:event andCell:cell]];
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
- (void)showContextualMenuForEvent:(MXEvent*)event fromSingleTapGesture:(BOOL)usedSingleTapGesture cell:(id<MXKCellRendering>)cell animated:(BOOL)animated
|
||||
@@ -6025,20 +6179,33 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
}
|
||||
|
||||
- (RoomContextualMenuItem *)copyMenuItemWithEvent:(MXEvent*)event andCell:(id<MXKCellRendering>)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<MXKCellRendering>)cell
|
||||
{
|
||||
MXKRoomBubbleTableViewCell *roomBubbleTableViewCell = (MXKRoomBubbleTableViewCell *)cell;
|
||||
MXKAttachment *attachment = roomBubbleTableViewCell.bubbleData.attachment;
|
||||
|
||||
MXWeakify(self);
|
||||
|
||||
BOOL isCopyActionEnabled = (event.eventType != MXEventTypePollStart && (!attachment || attachment.type != MXKAttachmentTypeSticker));
|
||||
BOOL result = (event.eventType != MXEventTypePollStart && (!attachment || attachment.type != MXKAttachmentTypeSticker));
|
||||
|
||||
if (attachment && !BuildSettings.messageDetailsAllowCopyMedia)
|
||||
{
|
||||
isCopyActionEnabled = NO;
|
||||
result = NO;
|
||||
}
|
||||
|
||||
if (isCopyActionEnabled)
|
||||
if (result)
|
||||
{
|
||||
switch (event.eventType) {
|
||||
case MXEventTypeRoomMessage:
|
||||
@@ -6047,7 +6214,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
|
||||
if ([messageType isEqualToString:kMXMessageTypeKeyVerificationRequest])
|
||||
{
|
||||
isCopyActionEnabled = NO;
|
||||
result = NO;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -6057,7 +6224,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
case MXEventTypeKeyVerificationMac:
|
||||
case MXEventTypeKeyVerificationDone:
|
||||
case MXEventTypeKeyVerificationCancel:
|
||||
isCopyActionEnabled = NO;
|
||||
result = NO;
|
||||
break;
|
||||
case MXEventTypeCustom:
|
||||
if ([event.type isEqualToString:kWidgetMatrixEventTypeString]
|
||||
@@ -6067,7 +6234,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
if ([widget.type isEqualToString:kWidgetTypeJitsiV1] ||
|
||||
[widget.type isEqualToString:kWidgetTypeJitsiV2])
|
||||
{
|
||||
isCopyActionEnabled = NO;
|
||||
result = NO;
|
||||
}
|
||||
}
|
||||
default:
|
||||
@@ -6075,60 +6242,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<MXKCellRendering>)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
|
||||
@@ -6150,6 +6317,23 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
return replyMenuItem;
|
||||
}
|
||||
|
||||
- (RoomContextualMenuItem *)replyInThreadMenuItemWithEvent:(MXEvent*)event
|
||||
{
|
||||
MXWeakify(self);
|
||||
|
||||
RoomContextualMenuItem *item = [[RoomContextualMenuItem alloc] initWithMenuAction:RoomContextualMenuActionReplyInThread];
|
||||
item.isEnabled = [self.roomDataSource canReplyToEventWithId:event.eventId] && !self.voiceMessageController.isRecordingAudio;
|
||||
item.action = ^{
|
||||
MXStrongifyAndReturnIfNil(self);
|
||||
|
||||
[self hideContextualMenuAnimated:YES cancelEventSelection:NO completion:nil];
|
||||
|
||||
[self openThreadWithId:event.eventId];
|
||||
};
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
- (RoomContextualMenuItem *)moreMenuItemWithEvent:(MXEvent*)event andCell:(id<MXKCellRendering>)cell
|
||||
{
|
||||
MXWeakify(self);
|
||||
@@ -6164,6 +6348,70 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
return moreMenuItem;
|
||||
}
|
||||
|
||||
#pragma mark - Threads
|
||||
|
||||
- (void)openThreadWithId:(NSString *)threadId
|
||||
{
|
||||
if (self.threadsBridgePresenter)
|
||||
{
|
||||
[self.threadsBridgePresenter dismissWithAnimated:YES completion:nil];
|
||||
self.threadsBridgePresenter = nil;
|
||||
}
|
||||
|
||||
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)highlightAndDisplayEvent:(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;
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
if (completion)
|
||||
{
|
||||
completion();
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - RoomContextualMenuViewControllerDelegate
|
||||
|
||||
- (void)roomContextualMenuViewControllerDidTapBackgroundOverlay:(RoomContextualMenuViewController *)viewController
|
||||
@@ -6563,4 +6811,29 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
[self mention:member];
|
||||
}
|
||||
|
||||
#pragma mark - ThreadsCoordinatorBridgePresenterDelegate
|
||||
|
||||
- (void)threadsCoordinatorBridgePresenterDelegateDidComplete:(ThreadsCoordinatorBridgePresenter *)coordinatorBridgePresenter
|
||||
{
|
||||
self.threadsBridgePresenter = nil;
|
||||
}
|
||||
|
||||
- (void)threadsCoordinatorBridgePresenterDelegateDidSelect:(ThreadsCoordinatorBridgePresenter *)coordinatorBridgePresenter roomId:(NSString *)roomId eventId:(NSString *)eventId
|
||||
{
|
||||
MXWeakify(self);
|
||||
[self.threadsBridgePresenter dismissWithAnimated:YES completion:^{
|
||||
MXStrongifyAndReturnIfNil(self);
|
||||
|
||||
if (eventId)
|
||||
{
|
||||
[self highlightAndDisplayEvent:eventId completion:nil];
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)threadsCoordinatorBridgePresenterDidDismissInteractively:(ThreadsCoordinatorBridgePresenter *)coordinatorBridgePresenter
|
||||
{
|
||||
self.threadsBridgePresenter = nil;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -18,9 +18,4 @@
|
||||
|
||||
@interface RoomFilesSearchViewController : MXKSearchViewController
|
||||
|
||||
/**
|
||||
The event selected in the search results
|
||||
*/
|
||||
@property (nonatomic, readonly) MXEvent *selectedEvent;
|
||||
|
||||
@end
|
||||
|
||||
@@ -170,18 +170,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
|
||||
|
||||
@@ -20,9 +20,4 @@
|
||||
|
||||
@interface RoomMessagesSearchViewController : MXKSearchViewController
|
||||
|
||||
/**
|
||||
The event selected in the search results
|
||||
*/
|
||||
@property (nonatomic, readonly) MXEvent *selectedEvent;
|
||||
|
||||
@end
|
||||
|
||||
@@ -206,18 +206,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
|
||||
|
||||
@@ -27,4 +27,6 @@
|
||||
|
||||
+ (instancetype)instantiate;
|
||||
|
||||
- (void)selectEvent:(MXEvent *)event;
|
||||
|
||||
@end
|
||||
|
||||
@@ -152,6 +152,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
|
||||
@@ -303,46 +328,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
|
||||
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
|
||||
|
||||
@@ -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<UIView*> *tmpSubviews;
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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<AnyHashable>) {
|
||||
cell.updateTickView(withFailedEventIds: failedEventIds)
|
||||
}
|
||||
|
||||
@@ -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<AnyHashable>)
|
||||
|
||||
@@ -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 ThreadSummaryModel {
|
||||
let numberOfReplies: Int
|
||||
let lastMessageSenderAvatar: AvatarViewDataProtocol?
|
||||
let lastMessageText: String?
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
//
|
||||
// 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 threadSummaryViewTapped(_ summaryView: ThreadSummaryView)
|
||||
}
|
||||
|
||||
/// 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 viewDefaultWidth: CGFloat = 320
|
||||
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?
|
||||
|
||||
private lazy var tapGestureRecognizer: UITapGestureRecognizer = {
|
||||
return UITapGestureRecognizer(target: self, action: #selector(tapped(_:)))
|
||||
}()
|
||||
|
||||
weak var delegate: ThreadSummaryViewDelegate?
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
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(withModel model: ThreadSummaryModel) {
|
||||
numberOfRepliesLabel.text = String(model.numberOfReplies)
|
||||
if let avatar = model.lastMessageSenderAvatar {
|
||||
lastMessageAvatarView.fill(with: avatar)
|
||||
} else {
|
||||
lastMessageAvatarView.avatarImageView.image = nil
|
||||
}
|
||||
lastMessageContentLabel.text = model.lastMessageText
|
||||
}
|
||||
|
||||
private func configure() {
|
||||
clipsToBounds = true
|
||||
layer.cornerRadius = Constants.cornerRadius
|
||||
addGestureRecognizer(tapGestureRecognizer)
|
||||
|
||||
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
|
||||
}
|
||||
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)
|
||||
|
||||
room.state { [weak self] roomState in
|
||||
guard let self = self else { return }
|
||||
let formatterError = UnsafeMutablePointer<MXKEventFormatterError>.allocate(capacity: 1)
|
||||
let lastMessageText = eventFormatter.string(from: lastMessage, with: roomState, error: formatterError)
|
||||
|
||||
let model = ThreadSummaryModel(numberOfReplies: thread.numberOfReplies,
|
||||
lastMessageSenderAvatar: avatarViewData,
|
||||
lastMessageText: lastMessageText)
|
||||
self.configure(withModel: model)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Action
|
||||
|
||||
@objc
|
||||
private func tapped(_ sender: UITapGestureRecognizer) {
|
||||
guard thread != nil else { return }
|
||||
delegate?.threadSummaryViewTapped(self)
|
||||
}
|
||||
}
|
||||
|
||||
extension ThreadSummaryView: NibOwnerLoadable {}
|
||||
|
||||
extension ThreadSummaryView: Themable {
|
||||
|
||||
func update(theme: Theme) {
|
||||
backgroundColor = theme.colors.system
|
||||
numberOfRepliesLabel.textColor = theme.colors.secondaryContent
|
||||
lastMessageContentLabel.textColor = theme.colors.secondaryContent
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="ThreadSummaryView" customModule="Riot" customModuleProvider="target">
|
||||
<connections>
|
||||
<outlet property="iconView" destination="vva-PV-3Ya" id="e1B-Zp-pni"/>
|
||||
<outlet property="lastMessageAvatarView" destination="9wW-1f-f69" id="xiI-t8-Q56"/>
|
||||
<outlet property="lastMessageContentLabel" destination="DVT-JI-3kw" id="O6N-ev-FRz"/>
|
||||
<outlet property="numberOfRepliesLabel" destination="GcG-W8-9LR" id="hzP-Ea-C6l"/>
|
||||
</connections>
|
||||
</placeholder>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<view contentMode="scaleToFill" id="iN0-l3-epB">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="32"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="TFL-sS-eJc">
|
||||
<rect key="frame" x="8" y="4" width="398" height="24"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="room_context_menu_reply_in_thread" translatesAutoresizingMaskIntoConstraints="NO" id="vva-PV-3Ya">
|
||||
<rect key="frame" x="1" y="3" width="18" height="18"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" secondItem="vva-PV-3Ya" secondAttribute="height" multiplier="1:1" id="972-WJ-2Zq"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="252" verticalHuggingPriority="251" text="1" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="GcG-W8-9LR">
|
||||
<rect key="frame" x="25" y="0.0" width="6" height="24"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="12"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="9wW-1f-f69" customClass="UserAvatarView" customModule="Riot" customModuleProvider="target">
|
||||
<rect key="frame" x="39" y="0.0" width="24" height="24"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" secondItem="9wW-1f-f69" secondAttribute="height" multiplier="1:1" id="V4H-JA-w4O"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="DVT-JI-3kw">
|
||||
<rect key="frame" x="71" y="0.0" width="319" height="24"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="12"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="vva-PV-3Ya" firstAttribute="top" secondItem="TFL-sS-eJc" secondAttribute="top" constant="3" id="3JV-P7-s7n"/>
|
||||
<constraint firstAttribute="bottom" secondItem="DVT-JI-3kw" secondAttribute="bottom" id="ArM-9P-35J"/>
|
||||
<constraint firstAttribute="bottom" secondItem="GcG-W8-9LR" secondAttribute="bottom" id="FI4-bk-goz"/>
|
||||
<constraint firstItem="9wW-1f-f69" firstAttribute="top" secondItem="TFL-sS-eJc" secondAttribute="top" id="GBe-gi-Iwc"/>
|
||||
<constraint firstItem="DVT-JI-3kw" firstAttribute="top" secondItem="TFL-sS-eJc" secondAttribute="top" id="MSs-PD-tov"/>
|
||||
<constraint firstItem="GcG-W8-9LR" firstAttribute="leading" secondItem="vva-PV-3Ya" secondAttribute="trailing" constant="6" id="PhI-J3-Ycb"/>
|
||||
<constraint firstItem="GcG-W8-9LR" firstAttribute="top" secondItem="TFL-sS-eJc" secondAttribute="top" id="Twp-gS-w3u"/>
|
||||
<constraint firstAttribute="bottom" secondItem="9wW-1f-f69" secondAttribute="bottom" id="VG5-XU-DAK"/>
|
||||
<constraint firstAttribute="trailing" secondItem="DVT-JI-3kw" secondAttribute="trailing" constant="8" id="bX2-Ha-8bf"/>
|
||||
<constraint firstItem="DVT-JI-3kw" firstAttribute="leading" secondItem="9wW-1f-f69" secondAttribute="trailing" constant="8" id="qGg-0A-C6M"/>
|
||||
<constraint firstItem="9wW-1f-f69" firstAttribute="leading" secondItem="GcG-W8-9LR" secondAttribute="trailing" constant="8" id="s2V-X9-cyI"/>
|
||||
<constraint firstAttribute="bottom" secondItem="vva-PV-3Ya" secondAttribute="bottom" constant="3" id="smY-cv-CoE"/>
|
||||
<constraint firstItem="vva-PV-3Ya" firstAttribute="leading" secondItem="TFL-sS-eJc" secondAttribute="leading" constant="1" id="vyh-e4-Vy3"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="bottom" secondItem="TFL-sS-eJc" secondAttribute="bottom" constant="4" id="GJq-Pw-T0A"/>
|
||||
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="TFL-sS-eJc" secondAttribute="trailing" constant="8" id="RR6-Uu-dD9"/>
|
||||
<constraint firstItem="TFL-sS-eJc" firstAttribute="leading" secondItem="iN0-l3-epB" secondAttribute="leading" constant="8" id="lbH-ay-p0c"/>
|
||||
<constraint firstItem="TFL-sS-eJc" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" constant="4" id="yqe-iO-Cz5"/>
|
||||
</constraints>
|
||||
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||
<point key="canvasLocation" x="-23.188405797101453" y="-179.46428571428569"/>
|
||||
</view>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="room_context_menu_reply_in_thread" width="18" height="18"/>
|
||||
</resources>
|
||||
</document>
|
||||
@@ -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 ThreadRoomTitleModel {
|
||||
let roomAvatar: AvatarViewDataProtocol?
|
||||
let roomEncryptionBadge: UIImage?
|
||||
let roomDisplayName: String?
|
||||
|
||||
static let empty = ThreadRoomTitleModel(roomAvatar: nil,
|
||||
roomEncryptionBadge: nil,
|
||||
roomDisplayName: nil)
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
enum ThreadRoomTitleViewMode {
|
||||
case allThreads
|
||||
case specificThread(threadId: String)
|
||||
}
|
||||
|
||||
@objcMembers
|
||||
class ThreadRoomTitleView: RoomTitleView {
|
||||
|
||||
private enum Constants {
|
||||
static let titleLeadingConstraintOnPortrait: CGFloat = 6
|
||||
static let titleLeadingConstraintOnLandscape: CGFloat = 18
|
||||
}
|
||||
|
||||
var mode: ThreadRoomTitleViewMode = .allThreads {
|
||||
didSet {
|
||||
update()
|
||||
}
|
||||
}
|
||||
|
||||
@IBOutlet private weak var titleLabel: UILabel!
|
||||
@IBOutlet private weak var titleLabelLeadingConstraint: NSLayoutConstraint!
|
||||
@IBOutlet private weak var roomAvatarView: RoomAvatarView!
|
||||
@IBOutlet private weak var roomEncryptionBadgeView: UIImageView!
|
||||
@IBOutlet private weak var roomNameLabel: UILabel!
|
||||
|
||||
// MARK: - Methods
|
||||
|
||||
func configure(withModel model: ThreadRoomTitleModel) {
|
||||
if let avatarViewData = model.roomAvatar {
|
||||
roomAvatarView.fill(with: avatarViewData)
|
||||
} else {
|
||||
roomAvatarView.avatarImageView.image = nil
|
||||
}
|
||||
roomEncryptionBadgeView.image = model.roomEncryptionBadge
|
||||
roomEncryptionBadgeView.isHidden = model.roomEncryptionBadge == nil
|
||||
roomNameLabel.text = model.roomDisplayName
|
||||
}
|
||||
|
||||
// MARK: - Overrides
|
||||
|
||||
override var mxRoom: MXRoom! {
|
||||
didSet {
|
||||
update()
|
||||
}
|
||||
}
|
||||
|
||||
override class func nib() -> UINib! {
|
||||
return self.nib
|
||||
}
|
||||
|
||||
override func refreshDisplay() {
|
||||
guard let room = mxRoom else {
|
||||
// room not initialized yet
|
||||
return
|
||||
}
|
||||
|
||||
let avatarViewData = AvatarViewData(matrixItemId: room.matrixItemId,
|
||||
displayName: room.displayName,
|
||||
avatarUrl: room.mxContentUri,
|
||||
mediaManager: room.mxSession.mediaManager,
|
||||
fallbackImage: AvatarFallbackImage.matrixItem(room.matrixItemId,
|
||||
room.displayName))
|
||||
|
||||
let encrpytionBadge: UIImage?
|
||||
if let summary = room.summary, room.mxSession.crypto != nil {
|
||||
encrpytionBadge = EncryptionTrustLevelBadgeImageHelper.roomBadgeImage(for: summary.roomEncryptionTrustLevel())
|
||||
} else {
|
||||
encrpytionBadge = nil
|
||||
}
|
||||
|
||||
let model = ThreadRoomTitleModel(roomAvatar: avatarViewData,
|
||||
roomEncryptionBadge: encrpytionBadge,
|
||||
roomDisplayName: room.displayName)
|
||||
configure(withModel: model)
|
||||
}
|
||||
|
||||
override func awakeFromNib() {
|
||||
super.awakeFromNib()
|
||||
|
||||
update(theme: ThemeService.shared().theme)
|
||||
registerThemeServiceDidChangeThemeNotification()
|
||||
}
|
||||
|
||||
override func updateLayout(for orientation: UIInterfaceOrientation) {
|
||||
super.updateLayout(for: orientation)
|
||||
|
||||
if orientation.isPortrait {
|
||||
titleLabelLeadingConstraint.constant = Constants.titleLeadingConstraintOnPortrait
|
||||
} else {
|
||||
titleLabelLeadingConstraint.constant = Constants.titleLeadingConstraintOnLandscape
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func registerThemeServiceDidChangeThemeNotification() {
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector: #selector(themeDidChange),
|
||||
name: .themeServiceDidChangeTheme,
|
||||
object: nil)
|
||||
}
|
||||
|
||||
private func update() {
|
||||
switch mode {
|
||||
case .allThreads:
|
||||
titleLabel.text = VectorL10n.threadsTitle
|
||||
case .specificThread:
|
||||
titleLabel.text = VectorL10n.roomThreadTitle
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
@objc private func themeDidChange() {
|
||||
self.update(theme: ThemeService.shared().theme)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ThreadRoomTitleView: NibLoadable {}
|
||||
|
||||
extension ThreadRoomTitleView: Themable {
|
||||
|
||||
func update(theme: Theme) {
|
||||
roomAvatarView.backgroundColor = .clear
|
||||
titleLabel.textColor = theme.colors.primaryContent
|
||||
roomNameLabel.textColor = theme.colors.secondaryContent
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<view contentMode="scaleToFill" id="iN0-l3-epB" customClass="ThreadRoomTitleView" customModule="Riot" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="243" height="64"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Ami-Cg-fcA">
|
||||
<rect key="frame" x="0.0" y="0.0" width="243" height="64"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Thread" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="BnG-NU-7Mg">
|
||||
<rect key="frame" x="18" y="22" width="56.5" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="FJB-2F-rrQ" customClass="RoomAvatarView" customModule="Riot" customModuleProvider="target">
|
||||
<rect key="frame" x="82.5" y="24" width="16" height="16"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="16" id="Fg7-y5-fEC"/>
|
||||
<constraint firstAttribute="height" constant="16" id="Qxm-RC-uC5"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="Mli-PC-WUh">
|
||||
<rect key="frame" x="92.5" y="28" width="12" height="12"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" secondItem="Mli-PC-WUh" secondAttribute="height" multiplier="1:1" id="Ohw-dy-qg0"/>
|
||||
<constraint firstAttribute="width" constant="12" id="nKB-SN-cO0"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Room name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="8lk-sN-3IP">
|
||||
<rect key="frame" x="109.5" y="24.5" width="67" height="15"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="12"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="Mli-PC-WUh" firstAttribute="centerX" secondItem="FJB-2F-rrQ" secondAttribute="trailing" id="BU4-yl-DrP"/>
|
||||
<constraint firstItem="BnG-NU-7Mg" firstAttribute="leading" secondItem="Ami-Cg-fcA" secondAttribute="leading" constant="18" id="ES6-mL-Y9F"/>
|
||||
<constraint firstItem="8lk-sN-3IP" firstAttribute="centerY" secondItem="Ami-Cg-fcA" secondAttribute="centerY" id="S0S-6y-Vkn"/>
|
||||
<constraint firstItem="FJB-2F-rrQ" firstAttribute="leading" secondItem="BnG-NU-7Mg" secondAttribute="trailing" constant="8" id="SQk-zN-CO6"/>
|
||||
<constraint firstItem="FJB-2F-rrQ" firstAttribute="centerY" secondItem="Ami-Cg-fcA" secondAttribute="centerY" id="nY0-2s-Wgo"/>
|
||||
<constraint firstItem="8lk-sN-3IP" firstAttribute="leading" secondItem="FJB-2F-rrQ" secondAttribute="trailing" constant="11" id="ql2-B3-82Y"/>
|
||||
<constraint firstItem="BnG-NU-7Mg" firstAttribute="centerY" secondItem="Ami-Cg-fcA" secondAttribute="centerY" id="rwC-ak-Ydb"/>
|
||||
<constraint firstItem="Mli-PC-WUh" firstAttribute="bottom" secondItem="FJB-2F-rrQ" secondAttribute="bottom" id="ulL-Xh-oCC"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="Ami-Cg-fcA" secondAttribute="trailing" id="a9m-d6-0go"/>
|
||||
<constraint firstAttribute="bottom" secondItem="Ami-Cg-fcA" secondAttribute="bottom" id="fEm-nj-yVF"/>
|
||||
<constraint firstItem="Ami-Cg-fcA" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" id="h0M-ab-EGv"/>
|
||||
<constraint firstItem="Ami-Cg-fcA" firstAttribute="top" secondItem="iN0-l3-epB" secondAttribute="top" id="zAN-VI-ZYk"/>
|
||||
</constraints>
|
||||
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||
<connections>
|
||||
<outlet property="roomAvatarView" destination="FJB-2F-rrQ" id="SUd-p8-VCH"/>
|
||||
<outlet property="roomEncryptionBadgeView" destination="Mli-PC-WUh" id="MuX-Qw-DfQ"/>
|
||||
<outlet property="roomNameLabel" destination="8lk-sN-3IP" id="wFm-R4-fBo"/>
|
||||
<outlet property="titleLabel" destination="BnG-NU-7Mg" id="gDw-Pr-oR8"/>
|
||||
<outlet property="titleLabelLeadingConstraint" destination="ES6-mL-Y9F" id="MpE-vt-KKC"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="0.7246376811594204" y="-152.00892857142856"/>
|
||||
</view>
|
||||
</objects>
|
||||
<resources>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
@@ -163,7 +163,8 @@ typedef NS_ENUM(NSUInteger, ABOUT)
|
||||
|
||||
typedef NS_ENUM(NSUInteger, LABS_ENABLE)
|
||||
{
|
||||
LABS_ENABLE_RINGING_FOR_GROUP_CALLS_INDEX
|
||||
LABS_ENABLE_RINGING_FOR_GROUP_CALLS_INDEX = 0,
|
||||
LABS_ENABLE_THREADS_INDEX
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSUInteger, SECURITY)
|
||||
@@ -578,7 +579,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)
|
||||
{
|
||||
@@ -2460,6 +2461,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;
|
||||
}
|
||||
}
|
||||
@@ -3194,6 +3207,13 @@ TableViewSectionsDelegate>
|
||||
RiotSettings.shared.enableRingingForGroupCalls = sender.isOn;
|
||||
}
|
||||
|
||||
- (void)toggleEnableThreads:(UISwitch *)sender
|
||||
{
|
||||
RiotSettings.shared.enableThreads = sender.isOn;
|
||||
[[MXKRoomDataSourceManager sharedManagerForMatrixSession:self.mainSession] reset];
|
||||
[[AppDelegate theDelegate] restoreEmptyDetailsViewController];
|
||||
}
|
||||
|
||||
- (void)togglePinRoomsWithMissedNotif:(UISwitch *)sender
|
||||
{
|
||||
RiotSettings.shared.pinRoomsWithMissedNotificationsOnHome = sender.isOn;
|
||||
|
||||
@@ -363,6 +363,30 @@ extension SplitViewCoordinator: SplitViewMasterPresentableDelegate {
|
||||
detailNavigationRouter.push(detailPresentable, animated: true, popCompletion: popCompletion)
|
||||
}
|
||||
|
||||
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: [NavigationModule]) {
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -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: [NavigationModule])
|
||||
|
||||
/// Stack modules on the existing split view detail stack
|
||||
func splitViewMasterPresentable(_ presentable: Presentable, wantsToStack modules: [NavigationModule])
|
||||
|
||||
/// 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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -381,25 +383,31 @@ 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)?) {
|
||||
|
||||
let roomCoordinatorParameters = RoomCoordinatorParameters(navigationRouterStore: NavigationRouterStore.shared,
|
||||
session: roomNavigationParameters.mxSession,
|
||||
roomId: roomNavigationParameters.roomId,
|
||||
eventId: roomNavigationParameters.eventId)
|
||||
|
||||
self.showRoom(with: roomCoordinatorParameters,
|
||||
stackOnSplitViewDetail: roomNavigationParameters.presentationParameters.stackAboveVisibleViews,
|
||||
completion: completion)
|
||||
if let threadParameters = roomNavigationParameters.threadParameters, threadParameters.stackRoomScreen {
|
||||
showRoomAndThread(with: roomNavigationParameters,
|
||||
completion: completion)
|
||||
} 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) {
|
||||
@@ -436,18 +444,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
|
||||
}
|
||||
|
||||
@@ -461,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
|
||||
|
||||
@@ -483,6 +552,14 @@ final class TabBarCoordinator: NSObject, TabBarCoordinatorType {
|
||||
}
|
||||
}
|
||||
|
||||
private func showSplitViewDetails(with modules: [NavigationModule], stack: Bool) {
|
||||
if stack {
|
||||
self.splitViewMasterPresentableDelegate?.splitViewMasterPresentable(self, wantsToStack: modules)
|
||||
} else {
|
||||
self.splitViewMasterPresentableDelegate?.splitViewMasterPresentable(self, wantsToReplaceDetailsWith: modules)
|
||||
}
|
||||
}
|
||||
|
||||
private func resetSplitViewDetails() {
|
||||
self.splitViewMasterPresentableDelegate?.splitViewMasterPresentableWantsToResetDetail(self)
|
||||
}
|
||||
@@ -620,9 +697,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
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
//
|
||||
// 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 {
|
||||
|
||||
// MARK: Private
|
||||
|
||||
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)
|
||||
threadVC.threadId = threadId
|
||||
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 setRoomTitleViewClass(_ roomTitleViewClass: AnyClass!) {
|
||||
super.setRoomTitleViewClass(ThreadRoomTitleView.self)
|
||||
|
||||
guard let threadTitleView = self.titleView as? ThreadRoomTitleView else {
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// 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,
|
||||
roomId: self.parameters.roomId)
|
||||
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 threadListViewModelDidLoadThreads(_ viewModel: ThreadListViewModelProtocol) {
|
||||
self.delegate?.threadListCoordinatorDidLoadThreads(self)
|
||||
}
|
||||
|
||||
func threadListViewModelDidSelectThread(_ viewModel: ThreadListViewModelProtocol, thread: MXThread) {
|
||||
self.delegate?.threadListCoordinatorDidSelectThread(self, thread: thread)
|
||||
}
|
||||
|
||||
func threadListViewModelDidCancel(_ viewModel: ThreadListViewModelProtocol) {
|
||||
self.delegate?.threadListCoordinatorDidCancel(self)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
/// ThreadListCoordinator input parameters
|
||||
struct ThreadListCoordinatorParameters {
|
||||
|
||||
/// The Matrix session
|
||||
let session: MXSession
|
||||
|
||||
/// Room identifier
|
||||
let roomId: String
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// 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 threadListCoordinatorDidLoadThreads(_ coordinator: ThreadListCoordinatorProtocol)
|
||||
func threadListCoordinatorDidSelectThread(_ coordinator: ThreadListCoordinatorProtocol, thread: MXThread)
|
||||
func threadListCoordinatorDidCancel(_ coordinator: ThreadListCoordinatorProtocol)
|
||||
}
|
||||
|
||||
/// `ThreadListCoordinatorProtocol` is a protocol describing a Coordinator that handle thread list navigation flow.
|
||||
protocol ThreadListCoordinatorProtocol: Coordinator, Presentable {
|
||||
var delegate: ThreadListCoordinatorDelegate? { get }
|
||||
}
|
||||
@@ -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
|
||||
|
||||
/// ThreadListViewController view actions exposed to view model
|
||||
enum ThreadListViewAction {
|
||||
case loadData
|
||||
case complete
|
||||
case showFilterTypes
|
||||
case selectFilterType(_ type: ThreadListFilterType)
|
||||
case selectThread(_ index: Int)
|
||||
case cancel
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="V8j-Lb-PgC">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Thread List View Controller-->
|
||||
<scene sceneID="mt5-wz-YKA">
|
||||
<objects>
|
||||
<viewController extendedLayoutIncludesOpaqueBars="YES" automaticallyAdjustsScrollViewInsets="NO" id="V8j-Lb-PgC" customClass="ThreadListViewController" customModule="Riot" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="EL9-GA-lwo">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="X8K-NO-SQ3">
|
||||
<rect key="frame" x="0.0" y="44" width="414" height="852"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="V8j-Lb-PgC" id="FCQ-5E-AuZ"/>
|
||||
<outlet property="delegate" destination="V8j-Lb-PgC" id="Kxs-vj-1RW"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<view contentMode="scaleToFill" placeholderIntrinsicWidth="414" placeholderIntrinsicHeight="818" translatesAutoresizingMaskIntoConstraints="NO" id="7VY-m9-wCS" customClass="ThreadListEmptyView" customModule="Riot" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="44" width="414" height="852"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="V8j-Lb-PgC" id="SQT-2N-wnf"/>
|
||||
</connections>
|
||||
</view>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="bFg-jh-JZB"/>
|
||||
<color key="backgroundColor" red="0.94509803921568625" green="0.96078431372549022" blue="0.97254901960784312" alpha="1" colorSpace="calibratedRGB"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="7VY-m9-wCS" secondAttribute="trailing" id="JYi-uG-Yqc"/>
|
||||
<constraint firstItem="X8K-NO-SQ3" firstAttribute="top" secondItem="bFg-jh-JZB" secondAttribute="top" id="Mfw-In-peq"/>
|
||||
<constraint firstAttribute="bottom" secondItem="7VY-m9-wCS" secondAttribute="bottom" id="PkX-MO-5aO"/>
|
||||
<constraint firstAttribute="bottom" secondItem="X8K-NO-SQ3" secondAttribute="bottom" id="SIG-7P-2CK"/>
|
||||
<constraint firstItem="7VY-m9-wCS" firstAttribute="leading" secondItem="EL9-GA-lwo" secondAttribute="leading" id="acw-H6-LKX"/>
|
||||
<constraint firstItem="X8K-NO-SQ3" firstAttribute="leading" secondItem="EL9-GA-lwo" secondAttribute="leading" id="ehI-Nc-6di"/>
|
||||
<constraint firstAttribute="trailing" secondItem="X8K-NO-SQ3" secondAttribute="trailing" id="hbZ-R8-8kH"/>
|
||||
<constraint firstItem="7VY-m9-wCS" firstAttribute="top" secondItem="bFg-jh-JZB" secondAttribute="top" id="obE-c9-v49"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="emptyView" destination="7VY-m9-wCS" id="Yny-Rn-edF"/>
|
||||
<outlet property="threadsTableView" destination="X8K-NO-SQ3" id="owA-Uh-r9B"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="zK0-v6-7Wt" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-3198.5507246376815" y="-647.54464285714278"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
@@ -0,0 +1,282 @@
|
||||
// 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: - Properties
|
||||
|
||||
// MARK: Outlets
|
||||
|
||||
@IBOutlet private weak var threadsTableView: UITableView!
|
||||
@IBOutlet private weak var emptyView: ThreadListEmptyView!
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private var viewModel: ThreadListViewModelProtocol!
|
||||
private var theme: Theme!
|
||||
private var keyboardAvoider: KeyboardAvoider?
|
||||
private var errorPresenter: MXKErrorPresentation!
|
||||
private var activityPresenter: ActivityIndicatorPresenter!
|
||||
private var titleView: ThreadRoomTitleView!
|
||||
|
||||
// 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.threadsTableView)
|
||||
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
|
||||
}
|
||||
|
||||
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
guard let titleView = self.titleView else { return }
|
||||
if UIApplication.shared.statusBarOrientation.isPortrait {
|
||||
titleView.updateLayout(for: .landscapeLeft)
|
||||
} else {
|
||||
titleView.updateLayout(for: .portrait)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
emptyView.update(theme: theme)
|
||||
emptyView.backgroundColor = theme.colors.background
|
||||
self.threadsTableView.backgroundColor = theme.backgroundColor
|
||||
self.threadsTableView.separatorColor = theme.colors.separator
|
||||
self.threadsTableView.reloadData()
|
||||
}
|
||||
|
||||
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 titleView = ThreadRoomTitleView.loadFromNib()
|
||||
titleView.mode = .allThreads
|
||||
titleView.configure(withModel: viewModel.titleModel)
|
||||
titleView.updateLayout(for: UIApplication.shared.statusBarOrientation)
|
||||
self.titleView = titleView
|
||||
navigationItem.leftItemsSupplementBackButton = true
|
||||
vc_removeBackTitle()
|
||||
navigationItem.leftBarButtonItem = UIBarButtonItem(customView: titleView)
|
||||
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(image: Asset.Images.threadsFilter.image,
|
||||
style: .plain,
|
||||
target: self,
|
||||
action: #selector(filterButtonTapped(_:)))
|
||||
|
||||
self.threadsTableView.tableFooterView = UIView()
|
||||
self.threadsTableView.register(cellType: ThreadTableViewCell.self)
|
||||
self.threadsTableView.keyboardDismissMode = .interactive
|
||||
}
|
||||
|
||||
private func render(viewState: ThreadListViewState) {
|
||||
switch viewState {
|
||||
case .idle:
|
||||
break
|
||||
case .loading:
|
||||
self.renderLoading()
|
||||
case .loaded:
|
||||
self.renderLoaded()
|
||||
case .empty(let model):
|
||||
self.renderEmptyView(withModel: model)
|
||||
case .showingFilterTypes:
|
||||
self.renderShowingFilterTypes()
|
||||
case .error(let error):
|
||||
self.render(error: error)
|
||||
}
|
||||
}
|
||||
|
||||
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(withModel model: ThreadListEmptyModel) {
|
||||
self.activityPresenter.removeCurrentActivityIndicator(animated: true)
|
||||
emptyView.configure(withModel: model)
|
||||
threadsTableView.isHidden = true
|
||||
emptyView.isHidden = false
|
||||
navigationItem.rightBarButtonItem?.isEnabled = viewModel.selectedFilterType == .myThreads
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
@objc
|
||||
private func filterButtonTapped(_ sender: UIBarButtonItem) {
|
||||
self.viewModel.process(viewAction: .showFilterTypes)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - ThreadListViewModelViewDelegate
|
||||
|
||||
extension ThreadListViewController: ThreadListViewModelViewDelegate {
|
||||
|
||||
func threadListViewModel(_ viewModel: ThreadListViewModelProtocol,
|
||||
didUpdateViewState viewSate: ThreadListViewState) {
|
||||
self.render(viewState: viewSate)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDataSource
|
||||
|
||||
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 threadModel = viewModel.threadModel(at: indexPath.row) {
|
||||
cell.configure(withModel: threadModel)
|
||||
}
|
||||
cell.update(theme: theme)
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDelegate
|
||||
|
||||
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)
|
||||
|
||||
viewModel.process(viewAction: .selectThread(indexPath.row))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - ThreadListEmptyViewDelegate
|
||||
|
||||
extension ThreadListViewController: ThreadListEmptyViewDelegate {
|
||||
|
||||
func threadListEmptyViewTappedShowAllThreads(_ emptyView: ThreadListEmptyView) {
|
||||
viewModel.process(viewAction: .selectFilterType(.all))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
// 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 let roomId: String
|
||||
private var threads: [MXThread] = []
|
||||
private var eventFormatter: MXKEventFormatter?
|
||||
private var roomState: MXRoomState?
|
||||
|
||||
private var currentOperation: MXHTTPOperation?
|
||||
|
||||
// MARK: Public
|
||||
|
||||
weak var viewDelegate: ThreadListViewModelViewDelegate?
|
||||
weak var coordinatorDelegate: ThreadListViewModelCoordinatorDelegate?
|
||||
var selectedFilterType: ThreadListFilterType = .all
|
||||
|
||||
private(set) var viewState: ThreadListViewState = .idle {
|
||||
didSet {
|
||||
self.viewDelegate?.threadListViewModel(self, didUpdateViewState: viewState)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Setup
|
||||
|
||||
init(session: MXSession,
|
||||
roomId: String) {
|
||||
self.session = session
|
||||
self.roomId = roomId
|
||||
session.threadingService.addDelegate(self)
|
||||
}
|
||||
|
||||
deinit {
|
||||
session.threadingService.removeDelegate(self)
|
||||
self.cancelOperations()
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func process(viewAction: ThreadListViewAction) {
|
||||
switch viewAction {
|
||||
case .loadData:
|
||||
loadData()
|
||||
case .complete:
|
||||
coordinatorDelegate?.threadListViewModelDidLoadThreads(self)
|
||||
case .showFilterTypes:
|
||||
viewState = .showingFilterTypes
|
||||
case .selectFilterType(let type):
|
||||
selectedFilterType = type
|
||||
loadData()
|
||||
case .selectThread(let index):
|
||||
selectThread(index)
|
||||
case .cancel:
|
||||
cancelOperations()
|
||||
coordinatorDelegate?.threadListViewModelDidCancel(self)
|
||||
}
|
||||
}
|
||||
|
||||
var numberOfThreads: Int {
|
||||
return threads.count
|
||||
}
|
||||
|
||||
func threadModel(at index: Int) -> ThreadModel? {
|
||||
guard index < threads.count else {
|
||||
return nil
|
||||
}
|
||||
return model(forThread: threads[index])
|
||||
}
|
||||
|
||||
var titleModel: ThreadRoomTitleModel {
|
||||
guard let room = session.room(withRoomId: roomId) else {
|
||||
return .empty
|
||||
}
|
||||
|
||||
let avatarViewData = AvatarViewData(matrixItemId: room.matrixItemId,
|
||||
displayName: room.displayName,
|
||||
avatarUrl: room.mxContentUri,
|
||||
mediaManager: room.mxSession.mediaManager,
|
||||
fallbackImage: AvatarFallbackImage.matrixItem(room.matrixItemId,
|
||||
room.displayName))
|
||||
|
||||
let encrpytionBadge: UIImage?
|
||||
if let summary = room.summary, session.crypto != nil {
|
||||
encrpytionBadge = EncryptionTrustLevelBadgeImageHelper.roomBadgeImage(for: summary.roomEncryptionTrustLevel())
|
||||
} else {
|
||||
encrpytionBadge = nil
|
||||
}
|
||||
|
||||
return ThreadRoomTitleModel(roomAvatar: avatarViewData,
|
||||
roomEncryptionBadge: encrpytionBadge,
|
||||
roomDisplayName: room.displayName)
|
||||
}
|
||||
|
||||
private var emptyViewModel: ThreadListEmptyModel {
|
||||
switch selectedFilterType {
|
||||
case .all:
|
||||
return ThreadListEmptyModel(icon: Asset.Images.roomContextMenuReplyInThread.image,
|
||||
title: VectorL10n.threadsEmptyTitle,
|
||||
info: VectorL10n.threadsEmptyInfoAll,
|
||||
tip: VectorL10n.threadsEmptyTip,
|
||||
showAllThreadsButtonTitle: VectorL10n.threadsEmptyShowAllThreads,
|
||||
showAllThreadsButtonHidden: true)
|
||||
case .myThreads:
|
||||
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 model(forThread thread: MXThread) -> ThreadModel {
|
||||
let rootAvatarViewData: AvatarViewData?
|
||||
let rootMessageSender: MXUser?
|
||||
let lastAvatarViewData: AvatarViewData?
|
||||
let lastMessageSender: MXUser?
|
||||
let rootMessageText = rootMessageText(forThread: thread)
|
||||
let (lastMessageText, lastMessageTime) = lastMessageTextAndTime(forThread: thread)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
let summaryModel = ThreadSummaryModel(numberOfReplies: thread.numberOfReplies,
|
||||
lastMessageSenderAvatar: lastAvatarViewData,
|
||||
lastMessageText: lastMessageText)
|
||||
|
||||
return ThreadModel(rootMessageSenderAvatar: rootAvatarViewData,
|
||||
rootMessageSenderDisplayName: rootMessageSender?.displayname,
|
||||
rootMessageText: rootMessageText,
|
||||
lastMessageTime: lastMessageTime,
|
||||
summaryModel: summaryModel)
|
||||
}
|
||||
|
||||
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<MXKEventFormatterError>.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<MXKEventFormatterError>.allocate(capacity: 1)
|
||||
return (
|
||||
eventFormatter.string(from: message,
|
||||
with: roomState,
|
||||
error: formatterError),
|
||||
eventFormatter.dateString(from: message, withTime: true)
|
||||
)
|
||||
}
|
||||
|
||||
private func loadData(showLoading: Bool = true) {
|
||||
|
||||
if showLoading {
|
||||
viewState = .loading
|
||||
}
|
||||
|
||||
switch selectedFilterType {
|
||||
case .all:
|
||||
threads = session.threadingService.threads(inRoom: roomId)
|
||||
case .myThreads:
|
||||
threads = session.threadingService.participatedThreads(inRoom: roomId)
|
||||
}
|
||||
|
||||
if threads.isEmpty {
|
||||
viewState = .empty(emptyViewModel)
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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 selectThread(_ index: Int) {
|
||||
guard index < threads.count else {
|
||||
return
|
||||
}
|
||||
let thread = threads[index]
|
||||
coordinatorDelegate?.threadListViewModelDidSelectThread(self, thread: thread)
|
||||
}
|
||||
|
||||
private func cancelOperations() {
|
||||
self.currentOperation?.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
extension ThreadListViewModel: MXThreadingServiceDelegate {
|
||||
|
||||
func threadingServiceDidUpdateThreads(_ service: MXThreadingService) {
|
||||
loadData(showLoading: false)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// 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 threadListViewModelDidLoadThreads(_ viewModel: ThreadListViewModelProtocol)
|
||||
func threadListViewModelDidSelectThread(_ viewModel: ThreadListViewModelProtocol, thread: MXThread)
|
||||
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 }
|
||||
|
||||
var titleModel: ThreadRoomTitleModel { get }
|
||||
var selectedFilterType: ThreadListFilterType { get }
|
||||
var numberOfThreads: Int { get }
|
||||
func threadModel(at index: Int) -> ThreadModel?
|
||||
}
|
||||
|
||||
enum ThreadListFilterType {
|
||||
case all
|
||||
case myThreads
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .all:
|
||||
return VectorL10n.threadsActionAllThreads
|
||||
case .myThreads:
|
||||
return VectorL10n.threadsActionMyThreads
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
/// ThreadListViewController view state
|
||||
enum ThreadListViewState {
|
||||
case idle
|
||||
case loading
|
||||
case loaded
|
||||
case empty(_ viewModel: ThreadListEmptyModel)
|
||||
case showingFilterTypes
|
||||
case error(Error)
|
||||
}
|
||||
@@ -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 ThreadModel {
|
||||
let rootMessageSenderAvatar: AvatarViewDataProtocol?
|
||||
let rootMessageSenderDisplayName: String?
|
||||
let rootMessageText: String?
|
||||
let lastMessageTime: String?
|
||||
let summaryModel: ThreadSummaryModel?
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
//
|
||||
// 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(withModel model: ThreadModel) {
|
||||
if let rootAvatar = model.rootMessageSenderAvatar {
|
||||
rootMessageAvatarView.fill(with: rootAvatar)
|
||||
} else {
|
||||
rootMessageAvatarView.avatarImageView.image = nil
|
||||
}
|
||||
rootMessageSenderLabel.text = model.rootMessageSenderDisplayName
|
||||
rootMessageContentLabel.text = model.rootMessageText
|
||||
lastMessageTimeLabel.text = model.lastMessageTime
|
||||
if let summaryModel = model.summaryModel {
|
||||
summaryView.configure(withModel: summaryModel)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension ThreadTableViewCell: NibReusable {}
|
||||
|
||||
extension ThreadTableViewCell: Themable {
|
||||
|
||||
func update(theme: Theme) {
|
||||
rootMessageAvatarView.backgroundColor = .clear
|
||||
rootMessageContentLabel.textColor = theme.colors.primaryContent
|
||||
lastMessageTimeLabel.textColor = theme.colors.secondaryContent
|
||||
summaryView.update(theme: theme)
|
||||
summaryView.backgroundColor = .clear
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" rowHeight="104" id="KGk-i7-Jjw" customClass="ThreadTableViewCell" customModule="Riot" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="104"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="104"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="I32-A5-WWw" customClass="UserAvatarView" customModule="Riot" customModuleProvider="target">
|
||||
<rect key="frame" x="12" y="12" width="32" height="32"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="32" id="f2R-6E-jRr"/>
|
||||
<constraint firstAttribute="height" constant="32" id="uWM-eP-XnP"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="108-Xh-aZf">
|
||||
<rect key="frame" x="56" y="12" width="200" height="17"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="14"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="252" verticalHuggingPriority="251" text="Time" textAlignment="right" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="C2U-Ih-4Oh">
|
||||
<rect key="frame" x="264" y="12" width="28" height="15"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="12"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Message" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="xzR-f9-3qV">
|
||||
<rect key="frame" x="56" y="33" width="236" height="17"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Md3-uq-cSB" customClass="ThreadSummaryView" customModule="Riot" customModuleProvider="target">
|
||||
<rect key="frame" x="47" y="58" width="245" height="34"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
|
||||
</view>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="I32-A5-WWw" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" constant="12" id="28p-b3-xMJ"/>
|
||||
<constraint firstItem="108-Xh-aZf" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" constant="12" id="2Dt-BH-xjF"/>
|
||||
<constraint firstItem="Md3-uq-cSB" firstAttribute="top" secondItem="xzR-f9-3qV" secondAttribute="bottom" constant="8" id="6mB-Yd-Pyg"/>
|
||||
<constraint firstAttribute="bottom" secondItem="Md3-uq-cSB" secondAttribute="bottom" constant="12" id="Ppd-HN-Ehg"/>
|
||||
<constraint firstItem="I32-A5-WWw" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" constant="12" id="Trt-CK-Tly"/>
|
||||
<constraint firstItem="Md3-uq-cSB" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" constant="47" id="Vpf-02-TgV"/>
|
||||
<constraint firstAttribute="trailing" secondItem="xzR-f9-3qV" secondAttribute="trailing" constant="28" id="Zz9-PK-l9b"/>
|
||||
<constraint firstItem="C2U-Ih-4Oh" firstAttribute="leading" secondItem="108-Xh-aZf" secondAttribute="trailing" constant="8" id="bE8-Yy-3B9"/>
|
||||
<constraint firstItem="xzR-f9-3qV" firstAttribute="leading" secondItem="I32-A5-WWw" secondAttribute="trailing" constant="12" id="g8i-lt-K8f"/>
|
||||
<constraint firstItem="108-Xh-aZf" firstAttribute="leading" secondItem="I32-A5-WWw" secondAttribute="trailing" constant="12" id="sXf-FI-gD3"/>
|
||||
<constraint firstItem="xzR-f9-3qV" firstAttribute="top" secondItem="108-Xh-aZf" secondAttribute="bottom" constant="4" id="tQN-Rr-MIS"/>
|
||||
<constraint firstItem="C2U-Ih-4Oh" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" constant="12" id="u3s-nr-avO"/>
|
||||
<constraint firstAttribute="trailing" secondItem="Md3-uq-cSB" secondAttribute="trailing" constant="28" id="vxt-vD-jy8"/>
|
||||
<constraint firstAttribute="trailing" secondItem="C2U-Ih-4Oh" secondAttribute="trailing" constant="28" id="wNc-xV-uIR"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
<viewLayoutGuide key="safeArea" id="njF-e1-oar"/>
|
||||
<connections>
|
||||
<outlet property="lastMessageTimeLabel" destination="C2U-Ih-4Oh" id="pf3-df-T65"/>
|
||||
<outlet property="rootMessageAvatarView" destination="I32-A5-WWw" id="zJW-QQ-jsG"/>
|
||||
<outlet property="rootMessageContentLabel" destination="xzR-f9-3qV" id="97u-na-8XW"/>
|
||||
<outlet property="rootMessageSenderLabel" destination="108-Xh-aZf" id="nUc-qK-UCD"/>
|
||||
<outlet property="summaryView" destination="Md3-uq-cSB" id="3ye-77-1m6"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="2.8985507246376816" y="129.91071428571428"/>
|
||||
</tableViewCell>
|
||||
</objects>
|
||||
<resources>
|
||||
<systemColor name="systemBackgroundColor">
|
||||
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</systemColor>
|
||||
</resources>
|
||||
</document>
|
||||
@@ -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 ThreadListEmptyModel {
|
||||
let icon: UIImage?
|
||||
let title: String?
|
||||
let info: String?
|
||||
let tip: String?
|
||||
let showAllThreadsButtonTitle: String?
|
||||
let showAllThreadsButtonHidden: Bool
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
//
|
||||
// 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)
|
||||
}
|
||||
|
||||
/// 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?
|
||||
|
||||
@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(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 = model.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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="ThreadListEmptyView" customModule="Riot" customModuleProvider="target">
|
||||
<connections>
|
||||
<outlet property="iconBackgroundView" destination="TO4-Bz-2iH" id="J0d-n5-T7m"/>
|
||||
<outlet property="iconView" destination="96m-sr-xQJ" id="iKm-fe-wZ5"/>
|
||||
<outlet property="infoLabel" destination="OE7-gq-abZ" id="9QP-LH-Wvh"/>
|
||||
<outlet property="showAllThreadsButton" destination="uTW-xb-Z9y" id="phx-FN-Dn2"/>
|
||||
<outlet property="tipLabel" destination="RyB-Ah-jey" id="Rh7-W2-EDU"/>
|
||||
<outlet property="titleLabel" destination="0q6-zY-VZH" id="RCC-ZR-q4F"/>
|
||||
</connections>
|
||||
</placeholder>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<view contentMode="scaleToFill" id="iN0-l3-epB">
|
||||
<rect key="frame" x="0.0" y="0.0" width="437" height="540"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" distribution="equalSpacing" alignment="center" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="f50-47-PNo">
|
||||
<rect key="frame" x="20" y="126" width="397" height="288"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="lm2-HJ-sTN">
|
||||
<rect key="frame" x="78.5" y="0.0" width="240" height="1"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="1" id="SCQ-dJ-7RE"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<view clipsSubviews="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="TO4-Bz-2iH">
|
||||
<rect key="frame" x="166.5" y="21" width="64" height="64"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="room_context_menu_reply_in_thread" translatesAutoresizingMaskIntoConstraints="NO" id="96m-sr-xQJ">
|
||||
<rect key="frame" x="16" y="16" width="32" height="32"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="32" id="K7U-U0-prN"/>
|
||||
<constraint firstAttribute="width" secondItem="96m-sr-xQJ" secondAttribute="height" multiplier="1:1" id="Pgd-Qw-0Ju"/>
|
||||
</constraints>
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="64" id="3dO-ZF-YeI"/>
|
||||
<constraint firstItem="96m-sr-xQJ" firstAttribute="centerY" secondItem="TO4-Bz-2iH" secondAttribute="centerY" id="EEK-EC-o8J"/>
|
||||
<constraint firstAttribute="height" constant="64" id="iUz-gL-c7h"/>
|
||||
<constraint firstItem="96m-sr-xQJ" firstAttribute="centerX" secondItem="TO4-Bz-2iH" secondAttribute="centerX" id="yqs-Ua-JWD"/>
|
||||
</constraints>
|
||||
<userDefinedRuntimeAttributes>
|
||||
<userDefinedRuntimeAttribute type="number" keyPath="layer.cornerRadius">
|
||||
<integer key="value" value="32"/>
|
||||
</userDefinedRuntimeAttribute>
|
||||
</userDefinedRuntimeAttributes>
|
||||
</view>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Keep discussions organised with threads" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="0q6-zY-VZH">
|
||||
<rect key="frame" x="27.5" y="105" width="342.5" height="21.5"/>
|
||||
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="18"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Threads help keep your conversations on-topic and easy to track." textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="OE7-gq-abZ">
|
||||
<rect key="frame" x="3" y="146.5" width="391.5" height="36"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="15"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Tip: Use “Thread” option when selecting a message." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="RyB-Ah-jey">
|
||||
<rect key="frame" x="50.5" y="202.5" width="296.5" height="14.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="12"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="uTW-xb-Z9y">
|
||||
<rect key="frame" x="142" y="237" width="113" height="30"/>
|
||||
<state key="normal" title="Show all threads"/>
|
||||
<connections>
|
||||
<action selector="showAllThreadsButtonTapped:" destination="-1" eventType="touchUpInside" id="cX4-am-oWF"/>
|
||||
</connections>
|
||||
</button>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="TKE-Zn-n2Q">
|
||||
<rect key="frame" x="78.5" y="287" width="240" height="1"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="1" id="oXf-fF-mRt"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<viewLayoutGuide key="safeArea" id="vUN-kp-3ea"/>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<constraints>
|
||||
<constraint firstItem="f50-47-PNo" firstAttribute="centerY" secondItem="iN0-l3-epB" secondAttribute="centerY" id="GG5-ib-fd0"/>
|
||||
<constraint firstItem="vUN-kp-3ea" firstAttribute="trailing" secondItem="f50-47-PNo" secondAttribute="trailing" constant="20" id="VsT-ay-7ZE"/>
|
||||
<constraint firstItem="f50-47-PNo" firstAttribute="leading" secondItem="vUN-kp-3ea" secondAttribute="leading" constant="20" id="gnS-Sc-jsF"/>
|
||||
</constraints>
|
||||
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||
<point key="canvasLocation" x="-100.72463768115942" y="-9.375"/>
|
||||
</view>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="room_context_menu_reply_in_thread" width="18" height="18"/>
|
||||
</resources>
|
||||
</document>
|
||||
@@ -0,0 +1,188 @@
|
||||
// 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 selectedThreadCoordinator: RoomCoordinator?
|
||||
|
||||
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
|
||||
super.init()
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector: #selector(didPopModule(_:)),
|
||||
name: NavigationRouter.didPopModule,
|
||||
object: nil)
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
func start() {
|
||||
|
||||
let rootCoordinator: Coordinator & Presentable
|
||||
if let threadId = parameters.threadId {
|
||||
rootCoordinator = createThreadCoordinator(forThreadId: threadId)
|
||||
} else {
|
||||
rootCoordinator = 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 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
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
@objc
|
||||
private func didPopModule(_ notification: Notification) {
|
||||
guard let userInfo = notification.userInfo,
|
||||
let module = userInfo[NavigationRouter.NotificationUserInfoKey.module] as? Presentable,
|
||||
let selectedThreadCoordinator = selectedThreadCoordinator else {
|
||||
return
|
||||
}
|
||||
|
||||
if module.toPresentable() == selectedThreadCoordinator.toPresentable() {
|
||||
selectedThreadCoordinator.delegate = nil
|
||||
remove(childCoordinator: selectedThreadCoordinator)
|
||||
self.selectedThreadCoordinator = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func createThreadListCoordinator() -> ThreadListCoordinator {
|
||||
let coordinatorParameters = ThreadListCoordinatorParameters(session: self.parameters.session,
|
||||
roomId: self.parameters.roomId)
|
||||
let coordinator = ThreadListCoordinator(parameters: coordinatorParameters)
|
||||
coordinator.delegate = self
|
||||
return coordinator
|
||||
}
|
||||
|
||||
private func createThreadCoordinator(forThreadId threadId: String) -> RoomCoordinator {
|
||||
let parameters = RoomCoordinatorParameters(navigationRouter: navigationRouter,
|
||||
navigationRouterStore: nil,
|
||||
session: parameters.session,
|
||||
roomId: parameters.roomId,
|
||||
eventId: nil,
|
||||
threadId: threadId,
|
||||
displayConfiguration: .forThreads)
|
||||
let coordinator = RoomCoordinator(parameters: parameters)
|
||||
coordinator.delegate = self
|
||||
return coordinator
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UIAdaptivePresentationControllerDelegate
|
||||
extension ThreadsCoordinator: UIAdaptivePresentationControllerDelegate {
|
||||
|
||||
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
|
||||
self.delegate?.threadsCoordinatorDidDismissInteractively(self)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ThreadListCoordinatorDelegate
|
||||
extension ThreadsCoordinator: ThreadListCoordinatorDelegate {
|
||||
func threadListCoordinatorDidLoadThreads(_ coordinator: ThreadListCoordinatorProtocol) {
|
||||
|
||||
}
|
||||
|
||||
func threadListCoordinatorDidSelectThread(_ coordinator: ThreadListCoordinatorProtocol, thread: MXThread) {
|
||||
let roomCoordinator = createThreadCoordinator(forThreadId: thread.id)
|
||||
selectedThreadCoordinator = roomCoordinator
|
||||
roomCoordinator.start()
|
||||
self.add(childCoordinator: roomCoordinator)
|
||||
}
|
||||
|
||||
func threadListCoordinatorDidCancel(_ coordinator: ThreadListCoordinatorProtocol) {
|
||||
self.delegate?.threadsCoordinatorDidComplete(self)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - RoomCoordinatorDelegate
|
||||
|
||||
extension ThreadsCoordinator: RoomCoordinatorDelegate {
|
||||
|
||||
func roomCoordinatorDidLeaveRoom(_ coordinator: RoomCoordinatorProtocol) {
|
||||
|
||||
}
|
||||
|
||||
func roomCoordinatorDidCancelRoomPreview(_ coordinator: RoomCoordinatorProtocol) {
|
||||
|
||||
}
|
||||
|
||||
func roomCoordinator(_ coordinator: RoomCoordinatorProtocol, didSelectRoomWithId roomId: String, eventId: String?) {
|
||||
self.delegate?.threadsCoordinatorDidSelect(self, roomId: roomId, eventId: eventId)
|
||||
}
|
||||
|
||||
func roomCoordinatorDidDismissInteractively(_ coordinator: RoomCoordinatorProtocol) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
// 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 threadsCoordinatorBridgePresenterDelegateDidSelect(_ coordinatorBridgePresenter: ThreadsCoordinatorBridgePresenter,
|
||||
roomId: String,
|
||||
eventId: String?)
|
||||
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 let threadId: String?
|
||||
private var navigationType: NavigationType = .present
|
||||
private var coordinator: ThreadsCoordinator?
|
||||
|
||||
// MARK: Public
|
||||
|
||||
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,
|
||||
threadId: String?) {
|
||||
self.session = session
|
||||
self.roomId = roomId
|
||||
self.threadId = threadId
|
||||
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,
|
||||
threadId: self.threadId)
|
||||
|
||||
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,
|
||||
threadId: self.threadId,
|
||||
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
|
||||
|
||||
completion?()
|
||||
}
|
||||
case .push:
|
||||
// stop coordinator to pop modules as needed
|
||||
coordinator.stop()
|
||||
self.coordinator = nil
|
||||
|
||||
completion?()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ThreadsCoordinatorDelegate
|
||||
extension ThreadsCoordinatorBridgePresenter: ThreadsCoordinatorDelegate {
|
||||
|
||||
func threadsCoordinatorDidComplete(_ coordinator: ThreadsCoordinatorProtocol) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
// 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
|
||||
|
||||
/// 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())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// 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)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
/// `ThreadsCoordinatorProtocol` is a protocol describing a Coordinator that handle xxxxxxx navigation flow.
|
||||
protocol ThreadsCoordinatorProtocol: Coordinator, Presentable {
|
||||
var delegate: ThreadsCoordinatorDelegate? { get }
|
||||
}
|
||||
@@ -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 NavigationModule {
|
||||
/// Actual presentable of the module
|
||||
let presentable: Presentable
|
||||
|
||||
/// Block to be called when the module is popped
|
||||
let popCompletion: (() -> Void)?
|
||||
}
|
||||
|
||||
// MARK: - CustomStringConvertible
|
||||
|
||||
extension NavigationModule: CustomStringConvertible {
|
||||
|
||||
var description: String {
|
||||
return "NavigationModule: \(presentable), pop completion: \(String(describing: popCompletion))"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -117,13 +117,13 @@ final class NavigationRouter: NSObject, NavigationRouterType {
|
||||
self.didPushViewController(controller)
|
||||
}
|
||||
|
||||
func setModules(_ modules: [Presentable], hideNavigationBar: Bool, animated: Bool) {
|
||||
func setModules(_ modules: [NavigationModule], 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: [NavigationModule], 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")
|
||||
|
||||
|
||||
@@ -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: [NavigationModule], 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: [NavigationModule], 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: [NavigationModule], animated: Bool) {
|
||||
setModules(modules, hideNavigationBar: false, animated: animated)
|
||||
}
|
||||
|
||||
func setModules(_ modules: [Presentable], animated: Bool) {
|
||||
setModules(modules, hideNavigationBar: false, animated: animated)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Presentable <--> NavigationModule Transitive Methods
|
||||
|
||||
extension NavigationRouterType {
|
||||
|
||||
func setRootModule(_ module: NavigationModule) {
|
||||
setRootModule(module.presentable, popCompletion: module.popCompletion)
|
||||
}
|
||||
|
||||
func push(_ module: NavigationModule, 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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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() -> NavigationModule {
|
||||
return NavigationModule(presentable: self, popCompletion: nil)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -427,7 +427,7 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
if event.isReply() {
|
||||
let parser = MXReplyEventParser()
|
||||
let replyParts = parser.parse(event)
|
||||
notificationBody = replyParts.bodyParts.replyText
|
||||
notificationBody = replyParts?.bodyParts.replyText
|
||||
} else {
|
||||
notificationBody = messageContent
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -86,7 +86,7 @@ final class PollEditFormCoordinator: Coordinator, Presentable {
|
||||
|
||||
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.pollEditFormViewModel.dispatch(action: .stopLoading(nil))
|
||||
|
||||
@@ -70,6 +70,7 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel
|
||||
|
||||
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 }
|
||||
|
||||
@@ -100,7 +101,7 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel
|
||||
}
|
||||
|
||||
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?.viewModel.dispatch(action: .showClosingFailure)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -227,36 +227,38 @@
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (isEncrypted)
|
||||
{
|
||||
[MXFileStore setPreloadOptions:0];
|
||||
|
||||
|
||||
MXSession *session = [[MXSession alloc] initWithMatrixRestClient:account.mxRestClient];
|
||||
MXWeakify(session);
|
||||
[session setStore:fileStore success:^{
|
||||
MXStrongifyAndReturnIfNil(session);
|
||||
|
||||
|
||||
MXRoom *room = [MXRoom loadRoomFromStore:fileStore withRoomId:roomID matrixSession:session];
|
||||
|
||||
|
||||
// Do not warn for unknown devices. We have cross-signing now
|
||||
session.crypto.warnOnUnknowDevices = NO;
|
||||
|
||||
|
||||
[room sendTextMessage:intent.content
|
||||
threadId:nil
|
||||
success:^(NSString *eventId) {
|
||||
completeWithCode(INSendMessageIntentResponseCodeSuccess);
|
||||
} failure:^(NSError *error) {
|
||||
completeWithCode(INSendMessageIntentResponseCodeFailure);
|
||||
}];
|
||||
|
||||
|
||||
} failure:^(NSError *error) {
|
||||
completeWithCode(INSendMessageIntentResponseCodeFailure);
|
||||
}];
|
||||
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
[account.mxRestClient sendTextMessageToRoom:roomID
|
||||
threadId:nil
|
||||
text:intent.content
|
||||
success:^(NSString *eventId) {
|
||||
completeWithCode(INSendMessageIntentResponseCodeSuccess);
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Permalinks: Create for thread events & handle navigations.
|
||||
@@ -0,0 +1 @@
|
||||
Search: Navigate to thread view for search results in threads.
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user