diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index f4817a070..5f0b672af 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -295,7 +295,7 @@ final class BuildSettings: NSObject { // Timeline style static let roomScreenAllowTimelineStyleConfiguration: Bool = false static let roomScreenTimelineDefaultStyleIdentifier: RoomTimelineStyleIdentifier = .plain - static var roomScreenEnableMessageBubblesByDefault: Bool { + static var isRoomScreenEnableMessageBubblesByDefault: Bool { return self.roomScreenTimelineDefaultStyleIdentifier == .bubble } diff --git a/Podfile b/Podfile index 1b0d9e1c0..286135afb 100644 --- a/Podfile +++ b/Podfile @@ -87,6 +87,7 @@ abstract_target 'RiotPods' do import_SwiftUI_pods pod 'DGCollectionViewLeftAlignFlowLayout', '~> 1.0.4' + pod 'UICollectionViewRightAlignedLayout', '~> 0.0.3' pod 'KTCenterFlowLayout', '~> 1.3.1' pod 'ZXingObjC', '~> 3.6.5' pod 'FlowCommoniOS', '~> 1.12.0' diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_location.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Actions/action_location.imageset/Contents.json index 5bb98bf57..3007f6fc5 100644 --- a/Riot/Assets/Images.xcassets/Room/Actions/action_location.imageset/Contents.json +++ b/Riot/Assets/Images.xcassets/Room/Actions/action_location.imageset/Contents.json @@ -19,5 +19,8 @@ "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" } } diff --git a/Riot/Assets/Images.xcassets/Room/Actions/action_poll.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Actions/action_poll.imageset/Contents.json index 7c6421fc2..2839a73cc 100644 --- a/Riot/Assets/Images.xcassets/Room/Actions/action_poll.imageset/Contents.json +++ b/Riot/Assets/Images.xcassets/Room/Actions/action_poll.imageset/Contents.json @@ -19,5 +19,8 @@ "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" } } diff --git a/Riot/Assets/Images.xcassets/Room/file_attachment.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/file_attachment.imageset/Contents.json new file mode 100644 index 000000000..1b8f71d8d --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/file_attachment.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "file_attachment.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "file_attachment@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "file_attachment@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/file_attachment.imageset/file_attachment.png b/Riot/Assets/Images.xcassets/Room/file_attachment.imageset/file_attachment.png new file mode 100644 index 000000000..cbefc09be Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/file_attachment.imageset/file_attachment.png differ diff --git a/Riot/Assets/Images.xcassets/Room/file_attachment.imageset/file_attachment@2x.png b/Riot/Assets/Images.xcassets/Room/file_attachment.imageset/file_attachment@2x.png new file mode 100644 index 000000000..0ded843f2 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/file_attachment.imageset/file_attachment@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/file_attachment.imageset/file_attachment@3x.png b/Riot/Assets/Images.xcassets/Room/file_attachment.imageset/file_attachment@3x.png new file mode 100644 index 000000000..d2d626082 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/file_attachment.imageset/file_attachment@3x.png differ diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 0cc59b0e5..4f37740fe 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -1905,6 +1905,10 @@ Tap the + to start adding people."; "location_sharing_share_action" = "Share"; +"location_sharing_post_failure_title" = "We couldn’t send your location"; + +"location_sharing_post_failure_subtitle" = "%@ could not send your location. Please try again later."; + "location_sharing_loading_map_error_title" = "%@ could not load the map. Please try again later."; "location_sharing_locating_user_error_title" = "%@ could not access your location. Please try again later."; diff --git a/Riot/Assets/third_party_licenses.html b/Riot/Assets/third_party_licenses.html index ee1a8d284..8d973a9a6 100644 --- a/Riot/Assets/third_party_licenses.html +++ b/Riot/Assets/third_party_licenses.html @@ -1925,6 +1925,31 @@ Library. SOFTWARE.

+
  • + UICollectionViewRightAlignedLayout (https://github.com/mokagio/UICollectionViewRightAlignedLayout) +

    + The MIT License (MIT) +

    + Copyright (c) 2014 Giovanni Lodi +

    + Permission is hereby granted, free of charge, to any person obtaining a copy of + this software and associated documentation files (the "Software"), to deal in + the Software without restriction, including without limitation the rights to + use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + the Software, and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: +

    + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. +

    + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +

    +
  • diff --git a/Riot/Categories/MXEvent.swift b/Riot/Categories/MXEvent.swift new file mode 100644 index 000000000..5ba0c1c7d --- /dev/null +++ b/Riot/Categories/MXEvent.swift @@ -0,0 +1,29 @@ +// +// 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 MatrixSDK + +extension MXEvent { + + /// Get MXMessageType if any + var messageType: MXMessageType? { + guard let messageTypeString = self.content["msgtype"] as? String else { + return nil + } + return MXMessageType(identifier: messageTypeString) + } +} diff --git a/Riot/Categories/MXKRoomBubbleCellData+Riot.swift b/Riot/Categories/MXKRoomBubbleCellData+Riot.swift index b7e7eaa9c..bfb7d1846 100644 --- a/Riot/Categories/MXKRoomBubbleCellData+Riot.swift +++ b/Riot/Categories/MXKRoomBubbleCellData+Riot.swift @@ -18,16 +18,13 @@ import Foundation extension MXKRoomBubbleCellData { - /// Indicate true if the sender is the session user - var isSenderCurrentUser: Bool { - if let senderId = self.senderId, let currentUserId = self.mxSession.myUserId, senderId == currentUserId { - return true - } - return false - } - // Indicate true if the cell data is collapsable and collapsed var isCollapsableAndCollapsed: Bool { return self.collapsable && self.collapsed } + + var cellDataTag: RoomBubbleCellDataTag { + return RoomBubbleCellDataTag(rawValue: self.tag) ?? .message + } + } diff --git a/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.h b/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.h index 24a0ff136..cc20ce9d0 100644 --- a/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.h +++ b/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.h @@ -67,6 +67,17 @@ extern NSString *const kMXKRoomBubbleCellKeyVerificationIncomingRequestDeclinePr */ - (void)addTimestampLabelForComponent:(NSUInteger)componentIndex; +/** + Add timestamp label for a component in receiver. + + Note: The label added here is automatically removed when [didEndDisplay] is called. + + @param componentIndex index of the component in bubble message data + @param displayOnLeft Indicate true to display label on left and false to display on right + */ +- (void)addTimestampLabelForComponent:(NSUInteger)componentIndex + displayOnLeft:(BOOL)displayOnLeft; + /** Highlight a component in receiver. diff --git a/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m b/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m index 2d2a0dbfc..98fde6a11 100644 --- a/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m +++ b/Riot/Categories/MXKRoomBubbleTableViewCell+Riot.m @@ -36,6 +36,29 @@ NSString *const kMXKRoomBubbleCellKeyVerificationIncomingRequestDeclinePressed = @implementation MXKRoomBubbleTableViewCell (Riot) - (void)addTimestampLabelForComponent:(NSUInteger)componentIndex +{ + BOOL isFirstDisplayedComponent = (componentIndex == 0); + BOOL isLastMessageMostRecentComponent = NO; + + RoomBubbleCellData *roomBubbleCellData; + + if ([bubbleData isKindOfClass:RoomBubbleCellData.class]) + { + roomBubbleCellData = (RoomBubbleCellData*)bubbleData; + isFirstDisplayedComponent = (componentIndex == roomBubbleCellData.oldestComponentIndex); + isLastMessageMostRecentComponent = roomBubbleCellData.containsLastMessage && (componentIndex == roomBubbleCellData.mostRecentComponentIndex); + } + + // Display timestamp on the left for selected component when it cannot overlap other UI elements like user's avatar + BOOL displayLabelOnLeft = roomBubbleCellData.displayTimestampForSelectedComponentOnLeftWhenPossible + && !isLastMessageMostRecentComponent + && (!isFirstDisplayedComponent || roomBubbleCellData.shouldHideSenderInformation); + + [self addTimestampLabelForComponent:componentIndex displayOnLeft:displayLabelOnLeft]; +} + +- (void)addTimestampLabelForComponent:(NSUInteger)componentIndex + displayOnLeft:(BOOL)displayLabelOnLeft { MXKRoomBubbleComponent *component; @@ -49,7 +72,6 @@ NSString *const kMXKRoomBubbleCellKeyVerificationIncomingRequestDeclinePressed = if (component && component.date) { BOOL isFirstDisplayedComponent = (componentIndex == 0); - BOOL isLastMessageMostRecentComponent = NO; RoomBubbleCellData *roomBubbleCellData; @@ -57,14 +79,8 @@ NSString *const kMXKRoomBubbleCellKeyVerificationIncomingRequestDeclinePressed = { roomBubbleCellData = (RoomBubbleCellData*)bubbleData; isFirstDisplayedComponent = (componentIndex == roomBubbleCellData.oldestComponentIndex); - isLastMessageMostRecentComponent = roomBubbleCellData.containsLastMessage && (componentIndex == roomBubbleCellData.mostRecentComponentIndex); } - // Display timestamp on the left for selected component when it cannot overlap other UI elements like user's avatar - BOOL displayLabelOnLeft = roomBubbleCellData.displayTimestampForSelectedComponentOnLeftWhenPossible - && !isLastMessageMostRecentComponent - && ( !isFirstDisplayedComponent || roomBubbleCellData.shouldHideSenderInformation); - [self addTimestampLabelForComponentIndex:componentIndex isFirstDisplayedComponent:isFirstDisplayedComponent viewTag:componentIndex diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index 2c6b3aeef..eca6c861e 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -186,6 +186,7 @@ internal enum Asset { internal static let addParticipants = ImageAsset(name: "add_participants") internal static let detailsIcon = ImageAsset(name: "details_icon") internal static let editIcon = ImageAsset(name: "edit_icon") + internal static let fileAttachment = ImageAsset(name: "file_attachment") internal static let integrationsIcon = ImageAsset(name: "integrations_icon") internal static let linkIcon = ImageAsset(name: "link_icon") internal static let mainAliasIcon = ImageAsset(name: "main_alias_icon") diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 992396767..e55abd47e 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -2227,6 +2227,14 @@ public class VectorL10n: NSObject { public static var locationSharingOpenGoogleMaps: String { return VectorL10n.tr("Vector", "location_sharing_open_google_maps") } + /// %@ could not send your location. Please try again later. + public static func locationSharingPostFailureSubtitle(_ p1: String) -> String { + return VectorL10n.tr("Vector", "location_sharing_post_failure_subtitle", p1) + } + /// We couldn’t send your location + public static var locationSharingPostFailureTitle: String { + return VectorL10n.tr("Vector", "location_sharing_post_failure_title") + } /// Location sharing public static var locationSharingSettingsHeader: String { return VectorL10n.tr("Vector", "location_sharing_settings_header") diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift index 3f1eec4d6..1b1af2591 100644 --- a/Riot/Managers/Settings/RiotSettings.swift +++ b/Riot/Managers/Settings/RiotSettings.swift @@ -189,13 +189,10 @@ final class RiotSettings: NSObject { @UserDefault(key: "roomScreenAllowFilesAction", defaultValue: BuildSettings.roomScreenAllowFilesAction, storage: defaults) var roomScreenAllowFilesAction - @UserDefault(key: "roomScreenAllowLocationAction", defaultValue: false, storage: defaults) - var roomScreenAllowLocationAction - @UserDefault(key: "roomScreenShowsURLPreviews", defaultValue: true, storage: defaults) var roomScreenShowsURLPreviews - @UserDefault(key: "roomScreenEnableMessageBubbles", defaultValue: BuildSettings.roomScreenEnableMessageBubblesByDefault, storage: defaults) + @UserDefault(key: "roomScreenEnableMessageBubbles", defaultValue: BuildSettings.isRoomScreenEnableMessageBubblesByDefault, storage: defaults) var roomScreenEnableMessageBubbles var roomTimelineStyleIdentifier: RoomTimelineStyleIdentifier { diff --git a/Riot/Managers/Theme/Theme.swift b/Riot/Managers/Theme/Theme.swift index 8c6f52a66..0ce22be40 100644 --- a/Riot/Managers/Theme/Theme.swift +++ b/Riot/Managers/Theme/Theme.swift @@ -96,6 +96,12 @@ import DesignKit /// Color to use in shadows. Should be contrast to `backgroundColor`. var shadowColor: UIColor { get } + + // Timeline cells + + var roomCellIncomingBubbleBackgroundColor: UIColor { get } + + var roomCellOutgoingBubbleBackgroundColor: UIColor { get } // MARK: - Customisation methods diff --git a/Riot/Managers/Theme/Themes/DarkTheme.swift b/Riot/Managers/Theme/Themes/DarkTheme.swift index b6aabc3ff..6178bd255 100644 --- a/Riot/Managers/Theme/Themes/DarkTheme.swift +++ b/Riot/Managers/Theme/Themes/DarkTheme.swift @@ -91,6 +91,12 @@ class DarkTheme: NSObject, Theme { var shadowColor: UIColor = UIColor(rgb: 0xFFFFFF) var messageTickColor: UIColor = .white + + var roomCellIncomingBubbleBackgroundColor: UIColor { + return self.colors.system + } + + var roomCellOutgoingBubbleBackgroundColor: UIColor = UIColor(rgb: 0x133A34) func applyStyle(onTabBar tabBar: UITabBar) { tabBar.unselectedItemTintColor = self.tabBarUnselectedItemTintColor diff --git a/Riot/Managers/Theme/Themes/DefaultTheme.swift b/Riot/Managers/Theme/Themes/DefaultTheme.swift index 545b801b9..8a77f5b23 100644 --- a/Riot/Managers/Theme/Themes/DefaultTheme.swift +++ b/Riot/Managers/Theme/Themes/DefaultTheme.swift @@ -100,6 +100,10 @@ class DefaultTheme: NSObject, Theme { var shadowColor: UIColor = UIColor(rgb: 0x000000) + var roomCellIncomingBubbleBackgroundColor: UIColor = UIColor(rgb: 0xE8EDF4) + + var roomCellOutgoingBubbleBackgroundColor: UIColor = UIColor(rgb: 0xE7F8F3) + func applyStyle(onTabBar tabBar: UITabBar) { tabBar.unselectedItemTintColor = self.tabBarUnselectedItemTintColor tabBar.tintColor = self.tintColor diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKAttachment.h b/Riot/Modules/MatrixKit/Models/Room/MXKAttachment.h index 287b8ef8a..6743df728 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKAttachment.h +++ b/Riot/Modules/MatrixKit/Models/Room/MXKAttachment.h @@ -27,7 +27,7 @@ extern NSString * const kMXKAttachmentErrorDomain; /** List attachment types */ -typedef enum : NSUInteger { +typedef NS_ENUM(NSUInteger, MXKAttachmentType) { MXKAttachmentTypeUndefined, MXKAttachmentTypeImage, MXKAttachmentTypeAudio, @@ -35,8 +35,7 @@ typedef enum : NSUInteger { MXKAttachmentTypeVideo, MXKAttachmentTypeFile, MXKAttachmentTypeSticker - -} MXKAttachmentType; +}; /** `MXKAttachment` represents a room attachment. diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.m index a455996ef..dc658e680 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.m @@ -744,6 +744,27 @@ return NO; } +- (BOOL)isAttachment +{ + if (!self.attachment) + { + return NO; + } + + if (!attachment.contentURL || !attachment.contentInfo) { + return NO; + } + + switch (self.attachment.type) { + case MXKAttachmentTypeFile: + case MXKAttachmentTypeAudio: + case MXKAttachmentTypeVoiceMessage: + return YES; + default: + return NO; + } +} + - (void)setMaxTextViewWidth:(CGFloat)inMaxTextViewWidth { // Check change diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellDataStoring.h b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellDataStoring.h index 53b96037c..9f227b79a 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellDataStoring.h +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellDataStoring.h @@ -147,6 +147,11 @@ */ @property (nonatomic) BOOL isAttachmentWithIcon; +/** + YES when the bubble correspond to an attachment (audio, file...). + */ +@property (nonatomic, readonly) BOOL isAttachment; + /** Flag that indicates that self.attributedTextMessage will be not nil. This avoids the computation of self.attributedTextMessage that can take time. diff --git a/Riot/Modules/MediaPicker/MediaPickerViewController.m b/Riot/Modules/MediaPicker/MediaPickerViewController.m index 190af4d17..48ff40d19 100644 --- a/Riot/Modules/MediaPicker/MediaPickerViewController.m +++ b/Riot/Modules/MediaPicker/MediaPickerViewController.m @@ -145,16 +145,13 @@ // Force UI refresh according to selected media types - Set default media type if none. self.mediaTypes = _mediaTypes ? _mediaTypes : @[(NSString *)kUTTypeImage]; - // Check photo library access - [self checkPhotoLibraryAuthorizationStatus]; // Observe UIApplicationWillEnterForegroundNotification to refresh captures collection when app leaves the background state. UIApplicationWillEnterForegroundNotificationObserver = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationWillEnterForegroundNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif) { MXStrongifyAndReturnIfNil(self); - [self reloadRecentCapturesCollection]; - [self reloadUserLibraryAlbums]; + [self checkPhotoLibraryAuthorizationStatusAndReload]; }]; @@ -218,8 +215,7 @@ userAlbumsQueue = dispatch_queue_create("media.picker.user.albums", DISPATCH_QUEUE_SERIAL); } - [self reloadRecentCapturesCollection]; - [self reloadUserLibraryAlbums]; + [self checkPhotoLibraryAuthorizationStatusAndReload]; } - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id )coordinator @@ -231,29 +227,28 @@ [self updateRecentCapturesCollectionViewHeightIfNeeded]; }); } - -- (void)checkPhotoLibraryAuthorizationStatus + +- (void)checkPhotoLibraryAuthorizationStatusAndReload { [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) { switch (status) { - case PHAuthorizationStatusAuthorized: + case PHAuthorizationStatusAuthorized: { // Load recent captures if this is not already done - if (!self->recentCaptures.count) - { - dispatch_async(dispatch_get_main_queue(), ^{ - - [self reloadRecentCapturesCollection]; - [self reloadUserLibraryAlbums]; - - }); - } + dispatch_async(dispatch_get_main_queue(), ^{ + + [self reloadRecentCapturesCollection]; + [self reloadUserLibraryAlbums]; + + }); break; - default: + } + default:{ dispatch_async(dispatch_get_main_queue(), ^{ [self presentPermissionDeniedAlert]; }); break; + } } }]; } @@ -302,8 +297,7 @@ { _mediaTypes = mediaTypes; - [self reloadRecentCapturesCollection]; - [self reloadUserLibraryAlbums]; + [self checkPhotoLibraryAuthorizationStatusAndReload]; } } diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 2acf9f1a2..fb4594abc 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -2012,7 +2012,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [self.delegate roomViewControllerDidRequestPollCreationFormPresentation:self]; }]]; } - if (RiotSettings.shared.roomScreenAllowLocationAction) + if (BuildSettings.locationSharingEnabled) { [actionItems addObject:[[RoomActionItem alloc] initWithImage:[UIImage imageNamed:@"action_location"] andAction:^{ MXStrongifyAndReturnIfNil(self); @@ -2704,17 +2704,35 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; } else if (bubbleData.attachment.type == MXKAttachmentTypeVoiceMessage || bubbleData.attachment.type == MXKAttachmentTypeAudio) { - if (bubbleData.isPaginationFirstBubble) + if (bubbleData.isIncoming) { - cellIdentifier = RoomTimelineCellIdentifierVoiceMessageWithPaginationTitle; - } - else if (bubbleData.shouldHideSenderInformation) - { - cellIdentifier = RoomTimelineCellIdentifierVoiceMessageWithoutSenderInfo; + if (bubbleData.isPaginationFirstBubble) + { + cellIdentifier = RoomTimelineCellIdentifierIncomingVoiceMessageWithPaginationTitle; + } + else if (bubbleData.shouldHideSenderInformation) + { + cellIdentifier = RoomTimelineCellIdentifierIncomingVoiceMessageWithoutSenderInfo; + } + else + { + cellIdentifier = RoomTimelineCellIdentifierIncomingVoiceMessage; + } } else { - cellIdentifier = RoomTimelineCellIdentifierVoiceMessage; + if (bubbleData.isPaginationFirstBubble) + { + cellIdentifier = RoomTimelineCellIdentifierOutgoingVoiceMessageWithPaginationTitle; + } + else if (bubbleData.shouldHideSenderInformation) + { + cellIdentifier = RoomTimelineCellIdentifierOutgoingVoiceMessageWithoutSenderInfo; + } + else + { + cellIdentifier = RoomTimelineCellIdentifierOutgoingVoiceMessage; + } } } else if (bubbleData.tag == RoomBubbleCellDataTagPoll) @@ -2734,17 +2752,35 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; } else if (bubbleData.tag == RoomBubbleCellDataTagLocation) { - if (bubbleData.isPaginationFirstBubble) + if (bubbleData.isIncoming) { - cellIdentifier = RoomTimelineCellIdentifierLocationWithPaginationTitle; - } - else if (bubbleData.shouldHideSenderInformation) - { - cellIdentifier = RoomTimelineCellIdentifierLocationWithoutSenderInfo; + if (bubbleData.isPaginationFirstBubble) + { + cellIdentifier = RoomTimelineCellIdentifierIncomingLocationWithPaginationTitle; + } + else if (bubbleData.shouldHideSenderInformation) + { + cellIdentifier = RoomTimelineCellIdentifierIncomingLocationWithoutSenderInfo; + } + else + { + cellIdentifier = RoomTimelineCellIdentifierIncomingLocation; + } } else { - cellIdentifier = RoomTimelineCellIdentifierLocation; + if (bubbleData.isPaginationFirstBubble) + { + cellIdentifier = RoomTimelineCellIdentifierOutgoingLocationWithPaginationTitle; + } + else if (bubbleData.shouldHideSenderInformation) + { + cellIdentifier = RoomTimelineCellIdentifierOutgoingLocationWithoutSenderInfo; + } + else + { + cellIdentifier = RoomTimelineCellIdentifierOutgoingLocation; + } } } else if (bubbleData.isIncoming) @@ -2769,6 +2805,21 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierIncomingAttachmentEncrypted : RoomTimelineCellIdentifierIncomingAttachment; } } + else if (bubbleData.isAttachment) + { + if (bubbleData.isPaginationFirstBubble) + { + cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnailEncryptedWithPaginationTitle : RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnailWithPaginationTitle; + } + else if (bubbleData.shouldHideSenderInformation) + { + cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnailEncryptedWithoutSenderInfo : RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnailWithoutSenderInfo; + } + else + { + cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnailEncrypted : RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnail; + } + } else { if (bubbleData.isPaginationFirstBubble) @@ -2819,6 +2870,21 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierOutgoingAttachmentEncrypted : RoomTimelineCellIdentifierOutgoingAttachment; } } + else if (bubbleData.isAttachment) + { + if (bubbleData.isPaginationFirstBubble) + { + cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnailEncryptedWithPaginationTitle : RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnailWithPaginationTitle; + } + else if (bubbleData.shouldHideSenderInformation) + { + cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnailEncryptedWithoutSenderInfo : RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnailWithoutSenderInfo; + } + else + { + cellIdentifier = showEncryptionBadge ? RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnailEncrypted : RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnail; + } + } else { if (bubbleData.isPaginationFirstBubble) diff --git a/Riot/Modules/Room/Views/BubbleCells/BaseBubbleCell/BaseBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/BaseBubbleCell/BaseBubbleCell.swift index 02a92cb87..29192b639 100644 --- a/Riot/Modules/Room/Views/BubbleCells/BaseBubbleCell/BaseBubbleCell.swift +++ b/Riot/Modules/Room/Views/BubbleCells/BaseBubbleCell/BaseBubbleCell.swift @@ -34,6 +34,8 @@ class BaseBubbleCell: MXKRoomBubbleTableViewCell, BaseBubbleCellType { weak var bubbleCellContentView: BubbleCellContentView? + private(set) var theme: Theme? + // Overrides override var bubbleInfoContainer: UIView! { @@ -205,6 +207,7 @@ class BaseBubbleCell: MXKRoomBubbleTableViewCell, BaseBubbleCellType { // MARK: - Themable func update(theme: Theme) { + self.theme = theme self.bubbleCellContentView?.update(theme: theme) } diff --git a/Riot/Modules/Room/Views/BubbleCells/BaseBubbleCell/BubbleCellContentView.swift b/Riot/Modules/Room/Views/BubbleCells/BaseBubbleCell/BubbleCellContentView.swift index 5a15f64d5..7fc615f8b 100644 --- a/Riot/Modules/Room/Views/BubbleCells/BaseBubbleCell/BubbleCellContentView.swift +++ b/Riot/Modules/Room/Views/BubbleCells/BaseBubbleCell/BubbleCellContentView.swift @@ -36,6 +36,9 @@ final class BubbleCellContentView: UIView, NibLoadable { @IBOutlet weak var innerContentView: UIView! + @IBOutlet weak var innerContentViewLeadingConstraint: NSLayoutConstraint! + @IBOutlet weak var innerContentViewTrailingConstraint: NSLayoutConstraint! + @IBOutlet weak var encryptionStatusContainerView: UIView! @IBOutlet weak var encryptionImageView: UIImageView! diff --git a/Riot/Modules/Room/Views/BubbleCells/BaseBubbleCell/BubbleCellContentView.xib b/Riot/Modules/Room/Views/BubbleCells/BaseBubbleCell/BubbleCellContentView.xib index 1a5a30708..999546651 100644 --- a/Riot/Modules/Room/Views/BubbleCells/BaseBubbleCell/BubbleCellContentView.xib +++ b/Riot/Modules/Room/Views/BubbleCells/BaseBubbleCell/BubbleCellContentView.xib @@ -1,9 +1,9 @@ - + - + @@ -234,6 +234,8 @@ + + @@ -246,7 +248,7 @@ - + diff --git a/Riot/Modules/Room/Views/BubbleCells/Common/MXKRoomBubbleTableViewCell.h b/Riot/Modules/Room/Views/BubbleCells/Common/MXKRoomBubbleTableViewCell.h index b9fa3dd33..27efc0c4a 100644 --- a/Riot/Modules/Room/Views/BubbleCells/Common/MXKRoomBubbleTableViewCell.h +++ b/Riot/Modules/Room/Views/BubbleCells/Common/MXKRoomBubbleTableViewCell.h @@ -217,6 +217,7 @@ extern NSString *const kMXKRoomBubbleCellUrlItemInteraction; @property (weak, nonatomic) IBOutlet NSLayoutConstraint *attachViewTopConstraint; @property (weak, nonatomic) IBOutlet NSLayoutConstraint *attachViewBottomConstraint; @property (weak, nonatomic) IBOutlet NSLayoutConstraint *attachViewLeadingConstraint; +@property (weak, nonatomic) NSLayoutConstraint *attachViewTrailingConstraint; /** The constraints which defines the relationship between bubbleInfoContainer and its superview @@ -329,4 +330,7 @@ extern NSString *const kMXKRoomBubbleCellUrlItemInteraction; /// Add temporary subview to `tmpSubviews` property. - (void)addTemporarySubview:(UIView*)subview; +/// Called when content view cell is tapped +- (IBAction)onContentViewTap:(UITapGestureRecognizer*)sender; + @end diff --git a/Riot/Modules/Room/Views/BubbleCells/Common/MXKRoomBubbleTableViewCell.m b/Riot/Modules/Room/Views/BubbleCells/Common/MXKRoomBubbleTableViewCell.m index b337d2c0f..fff04b59a 100644 --- a/Riot/Modules/Room/Views/BubbleCells/Common/MXKRoomBubbleTableViewCell.m +++ b/Riot/Modules/Room/Views/BubbleCells/Common/MXKRoomBubbleTableViewCell.m @@ -1069,10 +1069,7 @@ static BOOL _disableLongPressGestureOnEvent; - (BOOL)isBubbleDataContainsFileAttachment { - return bubbleData.attachment - && (bubbleData.attachment.type == MXKAttachmentTypeFile || bubbleData.attachment.type == MXKAttachmentTypeAudio || bubbleData.attachment.type == MXKAttachmentTypeVoiceMessage) - && bubbleData.attachment.contentURL - && bubbleData.attachment.contentInfo; + return bubbleData.isAttachment; } - (MXKRoomBubbleComponent*)closestBubbleComponentForGestureRecognizer:(UIGestureRecognizer*)gestureRecognizer locationInView:(UIView*)view diff --git a/Riot/Modules/Room/Views/BubbleCells/Poll/PollBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/Poll/PollBubbleCell.swift index 671adb2e9..c9263a37b 100644 --- a/Riot/Modules/Room/Views/BubbleCells/Poll/PollBubbleCell.swift +++ b/Riot/Modules/Room/Views/BubbleCells/Poll/PollBubbleCell.swift @@ -49,7 +49,7 @@ class PollBubbleCell: SizableBaseBubbleCell, BubbleCellReactionsDisplayable { } // The normal flow for tapping on cell content views doesn't work for bubbles without attributed strings - func onContentViewTap(_ sender: UITapGestureRecognizer) { + override func onContentViewTap(_ sender: UITapGestureRecognizer) { guard let event = self.event else { return } diff --git a/Riot/Modules/Room/Views/BubbleCells/RoomTimelineCellIdentifier.h b/Riot/Modules/Room/Views/BubbleCells/RoomTimelineCellIdentifier.h index 35a3b500a..13d9e504a 100644 --- a/Riot/Modules/Room/Views/BubbleCells/RoomTimelineCellIdentifier.h +++ b/Riot/Modules/Room/Views/BubbleCells/RoomTimelineCellIdentifier.h @@ -67,6 +67,25 @@ typedef NS_ENUM(NSUInteger, RoomTimelineCellIdentifier) { RoomTimelineCellIdentifierOutgoingAttachmentEncryptedWithoutSenderInfo, RoomTimelineCellIdentifierOutgoingAttachmentEncryptedWithPaginationTitle, + // - Attachment without thumbnail + // --- Clear + RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnail, + RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnailWithoutSenderInfo, + RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnailWithPaginationTitle, + // --- Encrypted + RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnailEncrypted, + RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnailEncryptedWithoutSenderInfo, + RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnailEncryptedWithPaginationTitle, + // -- Outgoing + // --- Clear + RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnail, + RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnailWithoutSenderInfo, + RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnailWithPaginationTitle, + // --- Encrypted + RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnailEncrypted, + RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnailEncryptedWithoutSenderInfo, + RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnailEncryptedWithPaginationTitle, + // - Room membership RoomTimelineCellIdentifierMembership, RoomTimelineCellIdentifierMembershipWithPaginationTitle, @@ -92,19 +111,29 @@ typedef NS_ENUM(NSUInteger, RoomTimelineCellIdentifier) { RoomTimelineCellIdentifierGroupCallStatus, // - Voice message - RoomTimelineCellIdentifierVoiceMessage, - RoomTimelineCellIdentifierVoiceMessageWithoutSenderInfo, - RoomTimelineCellIdentifierVoiceMessageWithPaginationTitle, - + // -- Incoming + RoomTimelineCellIdentifierIncomingVoiceMessage, + RoomTimelineCellIdentifierIncomingVoiceMessageWithoutSenderInfo, + RoomTimelineCellIdentifierIncomingVoiceMessageWithPaginationTitle, + // -- Outgoing + RoomTimelineCellIdentifierOutgoingVoiceMessage, + RoomTimelineCellIdentifierOutgoingVoiceMessageWithoutSenderInfo, + RoomTimelineCellIdentifierOutgoingVoiceMessageWithPaginationTitle, + // - Poll RoomTimelineCellIdentifierPoll, RoomTimelineCellIdentifierPollWithoutSenderInfo, RoomTimelineCellIdentifierPollWithPaginationTitle, // - Location sharing - RoomTimelineCellIdentifierLocation, - RoomTimelineCellIdentifierLocationWithoutSenderInfo, - RoomTimelineCellIdentifierLocationWithPaginationTitle, + // -- Incoming + RoomTimelineCellIdentifierIncomingLocation, + RoomTimelineCellIdentifierIncomingLocationWithoutSenderInfo, + RoomTimelineCellIdentifierIncomingLocationWithPaginationTitle, + // -- Outgoing + RoomTimelineCellIdentifierOutgoingLocation, + RoomTimelineCellIdentifierOutgoingLocationWithoutSenderInfo, + RoomTimelineCellIdentifierOutgoingLocationWithPaginationTitle, // - Others RoomTimelineCellIdentifierEmpty, diff --git a/Riot/Modules/Room/Views/BubbleCells/Sticker/RoomSelectedStickerBubbleCell.m b/Riot/Modules/Room/Views/BubbleCells/Sticker/RoomSelectedStickerBubbleCell.m index d5f48404a..cc0506fd2 100644 --- a/Riot/Modules/Room/Views/BubbleCells/Sticker/RoomSelectedStickerBubbleCell.m +++ b/Riot/Modules/Room/Views/BubbleCells/Sticker/RoomSelectedStickerBubbleCell.m @@ -121,6 +121,10 @@ } } + RoomTimelineConfiguration *timelineConfiguration = [RoomTimelineConfiguration shared]; + + [timelineConfiguration.currentStyle.cellLayoutUpdater updateLayoutForSelectedStickerCell:self]; + // Retrieve the suitable content size for the attachment thumbnail CGSize contentSize = bubbleData.contentSize; // Update image view frame in order to center loading wheel (if any) diff --git a/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/BubbleRoomCellLayoutUpdater.swift b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/BubbleRoomCellLayoutUpdater.swift index 42c0f3aac..b4c2a1c80 100644 --- a/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/BubbleRoomCellLayoutUpdater.swift +++ b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/BubbleRoomCellLayoutUpdater.swift @@ -24,11 +24,11 @@ class BubbleRoomCellLayoutUpdater: RoomCellLayoutUpdating { private var theme: Theme private var incomingColor: UIColor { - return self.theme.colors.system + return self.theme.roomCellIncomingBubbleBackgroundColor } private var outgoingColor: UIColor { - return self.theme.colors.accent.withAlphaComponent(0.10) + return self.theme.roomCellOutgoingBubbleBackgroundColor } // MARK: - Setup @@ -41,10 +41,10 @@ class BubbleRoomCellLayoutUpdater: RoomCellLayoutUpdating { func updateLayoutIfNeeded(for cell: MXKRoomBubbleTableViewCell, andCellData cellData: MXKRoomBubbleCellData) { - if cellData.isSenderCurrentUser { - self.updateLayout(forOutgoingTextMessageCell: cell, andCellData: cellData) - } else { + if cellData.isIncoming { self.updateLayout(forIncomingTextMessageCell: cell, andCellData: cellData) + } else { + self.updateLayout(forOutgoingTextMessageCell: cell, andCellData: cellData) } } @@ -107,6 +107,22 @@ class BubbleRoomCellLayoutUpdater: RoomCellLayoutUpdating { self.setupOutgoingFileAttachViewMargins(for: cell) } + func setupLayout(forIncomingFileAttachmentCell cell: MXKRoomBubbleTableViewCell) { + + self.setupIncomingFileAttachViewMargins(for: cell) + } + + func updateLayout(forSelectedStickerCell cell: RoomSelectedStickerBubbleCell) { + + if cell.bubbleData.isIncoming { + self.setupLayout(forIncomingFileAttachmentCell: cell) + } else { + self.setupLayout(forOutgoingFileAttachmentCell: cell) + cell.userNameLabel?.isHidden = true + cell.pictureView?.isHidden = true + } + } + // MARK: Themable func update(theme: Theme) { @@ -160,13 +176,13 @@ class BubbleRoomCellLayoutUpdater: RoomCellLayoutUpdating { switch firstEvent.eventType { case .roomMessage: - if let messageTypeString = firstEvent.content["msgtype"] as? String { - - let messageType = MXMessageType(identifier: messageTypeString) - + if let messageType = firstEvent.messageType { switch messageType { - case .text, .emote, .file: + case .text, .file: return true + case .emote: + // Explicitely disable bubble for emotes + return false default: break } @@ -341,5 +357,33 @@ class BubbleRoomCellLayoutUpdater: RoomCellLayoutUpdating { NSLayoutConstraint.activate([ rightConstraint ]) + + cell.attachViewTrailingConstraint = rightConstraint + } + + private func setupIncomingFileAttachViewMargins(for cell: MXKRoomBubbleTableViewCell) { + + guard let attachmentView = cell.attachmentView, + cell.attachViewLeadingConstraint == nil || cell.attachViewLeadingConstraint.isActive == false else { + return + } + + if let attachViewTrailingConstraint = cell.attachViewTrailingConstraint { + attachViewTrailingConstraint.isActive = false + cell.attachViewTrailingConstraint = nil + } + + let contentView = cell.contentView + + // TODO: Use constants + let leftMargin: CGFloat = 67 + + let leftConstraint = attachmentView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: -leftMargin) + + NSLayoutConstraint.activate([ + leftConstraint + ]) + + cell.attachViewLeadingConstraint = leftConstraint } } diff --git a/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/BubbleRoomTimelineCellDecorator.swift b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/BubbleRoomTimelineCellDecorator.swift index 2ea6ab381..b12502d9c 100644 --- a/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/BubbleRoomTimelineCellDecorator.swift +++ b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/BubbleRoomTimelineCellDecorator.swift @@ -28,39 +28,61 @@ class BubbleRoomTimelineCellDecorator: PlainRoomTimelineCellDecorator { } override func addTimestampLabel(toCell cell: MXKRoomBubbleTableViewCell, cellData: RoomBubbleCellData) { - + // If cell contains a bubble background, add the timestamp inside of it - if let bubbleBackgroundView = cell.messageBubbleBackgroundView, bubbleBackgroundView.isHidden == false { - - let componentIndex = cellData.mostRecentComponentIndex - - guard let bubbleComponents = cellData.bubbleComponents, - componentIndex < bubbleComponents.count else { - return - } - - let component = bubbleComponents[componentIndex] - - let timestampLabel = self.createTimestampLabel(cellData: cellData, - bubbleComponent: component, - viewTag: componentIndex) - timestampLabel.translatesAutoresizingMaskIntoConstraints = false - - cell.addTemporarySubview(timestampLabel) - - bubbleBackgroundView.addSubview(timestampLabel) - - let rightMargin: CGFloat = 8.0 - let bottomMargin: CGFloat = 4.0 - - let trailingConstraint = timestampLabel.trailingAnchor.constraint(equalTo: bubbleBackgroundView.trailingAnchor, constant: -rightMargin) - - let bottomConstraint = timestampLabel.bottomAnchor.constraint(equalTo: bubbleBackgroundView.bottomAnchor, constant: -bottomMargin) - - NSLayoutConstraint.activate([ - trailingConstraint, - bottomConstraint - ]) + if let bubbleBackgroundView = cell.messageBubbleBackgroundView, bubbleBackgroundView.isHidden == false, let timestampLabel = self.createTimestampLabel(for: cellData) { + + self.addTimestampLabel(timestampLabel, + to: cell, + on: bubbleBackgroundView, + constrainingView: bubbleBackgroundView) + + } else if cellData.isAttachmentWithThumbnail { + + if cellData.attachment?.type == .sticker, + let attachmentView = cell.attachmentView, + let timestampLabel = self.createTimestampLabel(for: cellData) { + + // Prevent overlap with send status icon + let bottomMargin: CGFloat = 20.0 + let rightMargin: CGFloat = -27.0 + + self.addTimestampLabel(timestampLabel, + to: cell, + on: cell.contentView, + constrainingView: attachmentView, + rightMargin: rightMargin, + bottomMargin: bottomMargin) + + } else if let attachmentView = cell.attachmentView, let timestampLabel = self.createTimestampLabel(for: cellData, textColor: self.theme.baseIconPrimaryColor) { + // For media with thumbnail cells, add timestamp inside thumbnail + + self.addTimestampLabel(timestampLabel, + to: cell, + on: cell.contentView, + constrainingView: attachmentView) + + } else { + super.addTimestampLabel(toCell: cell, cellData: cellData) + } + } else if let voiceMessageCell = cell as? VoiceMessageBubbleCell, let playbackView = voiceMessageCell.playbackController?.playbackView, let timestampLabel = self.createTimestampLabel(for: cellData) { + + // Add timestamp on cell inherting from VoiceMessageBubbleCell + + self.addTimestampLabel(timestampLabel, + to: cell, + on: cell.contentView, + constrainingView: playbackView) + + } else if let fileWithoutThumbnailCell = cell as? FileWithoutThumbnailBaseBubbleCell, let fileAttachementView = fileWithoutThumbnailCell.fileAttachementView, let timestampLabel = self.createTimestampLabel(for: cellData) { + + // Add timestamp on cell inherting from VoiceMessageBubbleCell + + self.addTimestampLabel(timestampLabel, + to: cell, + on: fileAttachementView, + constrainingView: fileAttachementView) + } else { super.addTimestampLabel(toCell: cell, cellData: cellData) } @@ -86,24 +108,9 @@ class BubbleRoomTimelineCellDecorator: PlainRoomTimelineCellDecorator { let topMargin: CGFloat = 4.0 let leftMargin: CGFloat let rightMargin: CGFloat - - // Outgoing message - if cellData.isSenderCurrentUser { - reactionsView.alignment = .right - - // TODO: Use constants - var outgointLeftMargin: CGFloat = 80.0 - - if cellData.containsBubbleComponentWithEncryptionBadge { - outgointLeftMargin += RoomBubbleCellLayout.encryptedContentLeftMargin - } - - leftMargin = outgointLeftMargin - - // TODO: Use constants - rightMargin = 33 - } else { - // Incoming message + + // Incoming message + if cellData.isIncoming { var incomingLeftMargin = RoomBubbleCellLayout.reactionsViewLeftMargin @@ -117,6 +124,22 @@ class BubbleRoomTimelineCellDecorator: PlainRoomTimelineCellDecorator { let messageViewMarginRight: CGFloat = 42.0 rightMargin = messageViewMarginRight + } else { + // Outgoing message + + reactionsView.alignment = .right + + // TODO: Use constants + var outgoingLeftMargin: CGFloat = 80.0 + + if cellData.containsBubbleComponentWithEncryptionBadge { + outgoingLeftMargin += RoomBubbleCellLayout.encryptedContentLeftMargin + } + + leftMargin = outgoingLeftMargin + + // TODO: Use constants + rightMargin = 33 } let leadingConstraint = reactionsView.leadingAnchor.constraint(equalTo: cellContentView.leadingAnchor, constant: leftMargin) @@ -152,16 +175,10 @@ class BubbleRoomTimelineCellDecorator: PlainRoomTimelineCellDecorator { let leadingOrTrailingConstraint: NSLayoutConstraint - // Outgoing message - if cellData.isSenderCurrentUser { - - // TODO: Use constants - let rightMargin: CGFloat = 34.0 - - leadingOrTrailingConstraint = urlPreviewView.trailingAnchor.constraint(equalTo: cellContentView.trailingAnchor, constant: -rightMargin) - } else { - // Incoming message - + + // Incoming message + if cellData.isIncoming { + var leftMargin = RoomBubbleCellLayout.reactionsViewLeftMargin if cellData.containsBubbleComponentWithEncryptionBadge { leftMargin += RoomBubbleCellLayout.encryptedContentLeftMargin @@ -170,6 +187,13 @@ class BubbleRoomTimelineCellDecorator: PlainRoomTimelineCellDecorator { leftMargin-=5.0 leadingOrTrailingConstraint = urlPreviewView.leadingAnchor.constraint(equalTo: cellContentView.leadingAnchor, constant: leftMargin) + } else { + // Outgoing message + + // TODO: Use constants + let rightMargin: CGFloat = 34.0 + + leadingOrTrailingConstraint = urlPreviewView.trailingAnchor.constraint(equalTo: cellContentView.trailingAnchor, constant: -rightMargin) } let topMargin = contentViewPositionY + RoomBubbleCellLayout.urlPreviewViewTopMargin + RoomBubbleCellLayout.reactionsViewTopMargin @@ -183,14 +207,16 @@ class BubbleRoomTimelineCellDecorator: PlainRoomTimelineCellDecorator { // MARK: - Private - private func createTimestampLabel(cellData: MXKRoomBubbleCellData, bubbleComponent: MXKRoomBubbleComponent, viewTag: Int) -> UILabel { + // MARK: Timestamp management + + private func createTimestampLabel(cellData: MXKRoomBubbleCellData, bubbleComponent: MXKRoomBubbleComponent, viewTag: Int, textColor: UIColor) -> UILabel { let timeLabel = UILabel() timeLabel.text = cellData.eventFormatter.timeString(from: bubbleComponent.date) timeLabel.textAlignment = .right - timeLabel.textColor = ThemeService.shared().theme.textSecondaryColor - timeLabel.font = UIFont.systemFont(ofSize: 11, weight: .light) + timeLabel.textColor = textColor + timeLabel.font = self.theme.fonts.caption2 timeLabel.adjustsFontSizeToFitWidth = true timeLabel.tag = viewTag timeLabel.accessibilityIdentifier = "timestampLabel" @@ -198,6 +224,23 @@ class BubbleRoomTimelineCellDecorator: PlainRoomTimelineCellDecorator { return timeLabel } + func createTimestampLabel(for cellData: RoomBubbleCellData) -> UILabel? { + return self.createTimestampLabel(for: cellData, textColor: self.theme.textSecondaryColor) + } + + private func createTimestampLabel(for cellData: RoomBubbleCellData, textColor: UIColor) -> UILabel? { + + let componentIndex = cellData.mostRecentComponentIndex + + guard let bubbleComponents = cellData.bubbleComponents, componentIndex < bubbleComponents.count else { + return nil + } + + let component = bubbleComponents[componentIndex] + + return self.createTimestampLabel(cellData: cellData, bubbleComponent: component, viewTag: componentIndex, textColor: textColor) + } + private func canShowTimestamp(forCellData cellData: MXKRoomBubbleCellData) -> Bool { guard cellData.isCollapsableAndCollapsed == false else { @@ -208,12 +251,29 @@ class BubbleRoomTimelineCellDecorator: PlainRoomTimelineCellDecorator { return false } + switch cellData.cellDataTag { + case .location: + return true + default: + break + } + + if let attachmentType = cellData.attachment?.type { + switch attachmentType { + case .voiceMessage, .audio: + return true + default: + break + } + } + + if cellData.isAttachmentWithThumbnail { + return true + } + switch firstEvent.eventType { case .roomMessage: - if let messageTypeString = firstEvent.content["msgtype"] as? String { - - let messageType = MXMessageType(identifier: messageTypeString) - + if let messageType = firstEvent.messageType { switch messageType { case .text, .emote, .file: return true @@ -227,4 +287,26 @@ class BubbleRoomTimelineCellDecorator: PlainRoomTimelineCellDecorator { return false } + + private func addTimestampLabel(_ timestampLabel: UILabel, + to cell: MXKRoomBubbleTableViewCell, + on containerView: UIView, + constrainingView: UIView, + rightMargin: CGFloat = 8.0, + bottomMargin: CGFloat = 4.0) { + timestampLabel.translatesAutoresizingMaskIntoConstraints = false + + cell.addTemporarySubview(timestampLabel) + + containerView.addSubview(timestampLabel) + + let trailingConstraint = timestampLabel.trailingAnchor.constraint(equalTo: constrainingView.trailingAnchor, constant: -rightMargin) + + let bottomConstraint = timestampLabel.bottomAnchor.constraint(equalTo: constrainingView.bottomAnchor, constant: -bottomMargin) + + NSLayoutConstraint.activate([ + trailingConstraint, + bottomConstraint + ]) + } } diff --git a/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/BubbleRoomTimelineCellProvider.m b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/BubbleRoomTimelineCellProvider.m index f36d21fdf..b387a9b45 100644 --- a/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/BubbleRoomTimelineCellProvider.m +++ b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/BubbleRoomTimelineCellProvider.m @@ -51,8 +51,56 @@ #import "RoomOutgoingEncryptedAttachmentWithoutSenderInfoBubbleCell.h" #import "RoomOutgoingEncryptedAttachmentWithPaginationTitleBubbleCell.h" +#import "GeneratedInterface-Swift.h" + @implementation BubbleRoomTimelineCellProvider +#pragma mark - Registration + +- (void)registerCellsForTableView:(UITableView *)tableView +{ + [super registerCellsForTableView:tableView]; + + [self registerFileWithoutThumbnailCellsForTableView:tableView]; +} + +- (void)registerVoiceMessageCellsForTableView:(UITableView*)tableView +{ + // Incoming + [tableView registerClass:VoiceMessageIncomingBubbleCell.class forCellReuseIdentifier:VoiceMessageIncomingBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:VoiceMessageIncomingWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:VoiceMessageIncomingWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:VoiceMessageIncomingWithPaginationTitleBubbleCell.class forCellReuseIdentifier:VoiceMessageIncomingWithPaginationTitleBubbleCell.defaultReuseIdentifier]; + // Outgoing + [tableView registerClass:VoiceMessageOutgoingWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:VoiceMessageOutgoingWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:VoiceMessageOutgoingWithPaginationTitleBubbleCell.class forCellReuseIdentifier:VoiceMessageOutgoingWithPaginationTitleBubbleCell.defaultReuseIdentifier]; +} + +- (void)registerLocationCellsForTableView:(UITableView*)tableView +{ + // Incoming + [tableView registerClass:LocationIncomingBubbleCell.class forCellReuseIdentifier:LocationIncomingBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:LocationIncomingWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:LocationIncomingWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:LocationIncomingWithPaginationTitleBubbleCell.class forCellReuseIdentifier:LocationIncomingWithPaginationTitleBubbleCell.defaultReuseIdentifier]; + + // Outgoing + [tableView registerClass:LocationOutgoingWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:LocationOutgoingWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:LocationOutgoingWithPaginationTitleBubbleCell.class forCellReuseIdentifier:LocationOutgoingWithPaginationTitleBubbleCell.defaultReuseIdentifier]; +} + +- (void)registerFileWithoutThumbnailCellsForTableView:(UITableView*)tableView +{ + // Incoming + [tableView registerClass:FileWithoutThumbnailIncomingBubbleCell.class forCellReuseIdentifier:FileWithoutThumbnailIncomingBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:FileWithoutThumbnailIncomingWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:FileWithoutThumbnailIncomingWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:FileWithoutThumbnailIncomingWithPaginationTitleBubbleCell.class forCellReuseIdentifier:FileWithoutThumbnailIncomingWithPaginationTitleBubbleCell.defaultReuseIdentifier]; + + // Outgoing + [tableView registerClass:FileWithoutThumbnailOutoingWithoutSenderInfoBubbleCell.class forCellReuseIdentifier:FileWithoutThumbnailOutoingWithoutSenderInfoBubbleCell.defaultReuseIdentifier]; + [tableView registerClass:FileWithoutThumbnailOutoingWithPaginationTitleBubbleCell.class forCellReuseIdentifier:FileWithoutThumbnailOutoingWithPaginationTitleBubbleCell.defaultReuseIdentifier]; +} + +#pragma mark - Mapping + - (NSDictionary*)outgoingTextMessageCellsMapping { // Hide sender info and avatar for bubble outgoing messages @@ -87,4 +135,60 @@ }; } +- (NSDictionary*)incomingAttachmentWithoutThumbnailCellsMapping +{ + return @{ + // Clear + @(RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnail) : FileWithoutThumbnailIncomingBubbleCell.class, + @(RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnailWithoutSenderInfo) : FileWithoutThumbnailIncomingWithoutSenderInfoBubbleCell.class, + @(RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnailWithPaginationTitle) : FileWithoutThumbnailIncomingWithPaginationTitleBubbleCell.class, + // Encrypted + @(RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnailEncrypted) : FileWithoutThumbnailIncomingBubbleCell.class, + @(RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnailEncryptedWithoutSenderInfo) : FileWithoutThumbnailIncomingWithoutSenderInfoBubbleCell.class, + @(RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnailEncryptedWithPaginationTitle) : FileWithoutThumbnailIncomingWithPaginationTitleBubbleCell.class + }; +} + +- (NSDictionary*)outgoingAttachmentWithoutThumbnailCellsMapping +{ + return @{ + // Clear + @(RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnail) : FileWithoutThumbnailOutoingWithoutSenderInfoBubbleCell.class, + @(RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnailWithoutSenderInfo) : FileWithoutThumbnailOutoingWithoutSenderInfoBubbleCell.class, + @(RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnailWithPaginationTitle) : FileWithoutThumbnailOutoingWithPaginationTitleBubbleCell.class, + // Encrypted + @(RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnailEncrypted) : FileWithoutThumbnailOutoingWithoutSenderInfoBubbleCell.class, + @(RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnailEncryptedWithoutSenderInfo) : FileWithoutThumbnailOutoingWithoutSenderInfoBubbleCell.class, + @(RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnailEncryptedWithPaginationTitle) : FileWithoutThumbnailOutoingWithPaginationTitleBubbleCell.class + }; +} + +- (NSDictionary*)voiceMessageCellsMapping +{ + return @{ + // Incoming + @(RoomTimelineCellIdentifierIncomingVoiceMessage) : VoiceMessageIncomingBubbleCell.class, + @(RoomTimelineCellIdentifierIncomingVoiceMessageWithoutSenderInfo) : VoiceMessageIncomingWithoutSenderInfoBubbleCell.class, + @(RoomTimelineCellIdentifierIncomingVoiceMessageWithPaginationTitle) : VoiceMessageIncomingWithPaginationTitleBubbleCell.class, + // Outgoing + @(RoomTimelineCellIdentifierOutgoingVoiceMessage) : VoiceMessageOutgoingWithoutSenderInfoBubbleCell.class, + @(RoomTimelineCellIdentifierOutgoingVoiceMessageWithoutSenderInfo) : VoiceMessageOutgoingWithoutSenderInfoBubbleCell.class, + @(RoomTimelineCellIdentifierOutgoingVoiceMessageWithPaginationTitle) : VoiceMessageOutgoingWithPaginationTitleBubbleCell.class, + }; +} + +- (NSDictionary*)locationCellsMapping +{ + return @{ + // Incoming + @(RoomTimelineCellIdentifierIncomingLocation) : LocationIncomingBubbleCell.class, + @(RoomTimelineCellIdentifierIncomingLocationWithoutSenderInfo) : LocationIncomingWithoutSenderInfoBubbleCell.class, + @(RoomTimelineCellIdentifierIncomingLocationWithPaginationTitle) : LocationIncomingWithPaginationTitleBubbleCell.class, + // Outgoing + @(RoomTimelineCellIdentifierOutgoingLocation) : LocationOutgoingWithoutSenderInfoBubbleCell.class, + @(RoomTimelineCellIdentifierOutgoingLocationWithoutSenderInfo) : LocationOutgoingWithoutSenderInfoBubbleCell.class, + @(RoomTimelineCellIdentifierOutgoingLocationWithPaginationTitle) : LocationOutgoingWithPaginationTitleBubbleCell.class + }; +} + @end diff --git a/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/FileWithoutThumbnail/Common/FileWithoutThumbnailBaseBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/FileWithoutThumbnail/Common/FileWithoutThumbnailBaseBubbleCell.swift new file mode 100644 index 000000000..65183f299 --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/FileWithoutThumbnail/Common/FileWithoutThumbnailBaseBubbleCell.swift @@ -0,0 +1,59 @@ +// +// 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 + +class FileWithoutThumbnailBaseBubbleCell: SizableBaseBubbleCell, BubbleCellReactionsDisplayable { + + weak var fileAttachementView: FileWithoutThumbnailCellContentView? + + override func render(_ cellData: MXKCellData!) { + super.render(cellData) + + guard let data = cellData as? RoomBubbleCellData else { + return + } + + self.fileAttachementView?.title = data.attributedTextMessage.string + + self.update(theme: ThemeService.shared().theme) + } + + override func setupViews() { + super.setupViews() + + bubbleCellContentView?.backgroundColor = .clear + + guard let contentView = bubbleCellContentView?.innerContentView else { + return + } + + let fileAttachementView = FileWithoutThumbnailCellContentView.instantiate() + + contentView.vc_addSubViewMatchingParent(fileAttachementView) + + self.fileAttachementView = fileAttachementView + } + + override func onContentViewTap(_ sender: UITapGestureRecognizer!) { + + if let bubbleData = self.bubbleData, bubbleData.isAttachment { + self.delegate.cell(self, didRecognizeAction: kMXKRoomBubbleCellTapOnAttachmentView, userInfo: nil) + } else { + super.onContentViewTap(sender) + } + } +} diff --git a/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/FileWithoutThumbnail/Common/FileWithoutThumbnailCellContentView.swift b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/FileWithoutThumbnail/Common/FileWithoutThumbnailCellContentView.swift new file mode 100644 index 000000000..14dd3c7fa --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/FileWithoutThumbnail/Common/FileWithoutThumbnailCellContentView.swift @@ -0,0 +1,82 @@ +// +// 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 + +final class FileWithoutThumbnailCellContentView: UIView, NibLoadable { + + // MARK: - Constants + + private enum Constants { + // TODO: Reuse constants, same as bubble bg + static let cornerRadius: CGFloat = 12.0 + } + + // MARK: - Properties + + // MARK: Outlets + + @IBOutlet private weak var iconImageView: UIImageView! + @IBOutlet private weak var titleLabel: UILabel! + + // MARK: Public + + var badgeImage: UIImage? { + get { + return self.iconImageView.image + } + set { + self.iconImageView.image = newValue + } + } + + var title: String? { + get { + return self.titleLabel.text + } + set { + self.titleLabel.text = newValue + } + } + + // MARK: - Setup + + static func instantiate() -> FileWithoutThumbnailCellContentView { + return FileWithoutThumbnailCellContentView.loadFromNib() + } + + // MARK: - Life cycle + + override func awakeFromNib() { + super.awakeFromNib() + + self.layer.masksToBounds = true + } + + override func layoutSubviews() { + super.layoutSubviews() + + self.layer.cornerRadius = Constants.cornerRadius + } + + // MARK: - Public + + func update(theme: Theme) { + self.titleLabel.textColor = theme.textPrimaryColor + } +} + diff --git a/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/FileWithoutThumbnail/Common/FileWithoutThumbnailCellContentView.xib b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/FileWithoutThumbnail/Common/FileWithoutThumbnailCellContentView.xib new file mode 100644 index 000000000..21e33aef7 --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/FileWithoutThumbnail/Common/FileWithoutThumbnailCellContentView.xib @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/FileWithoutThumbnail/Incoming/FileWithoutThumbnailIncomingBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/FileWithoutThumbnail/Incoming/FileWithoutThumbnailIncomingBubbleCell.swift new file mode 100644 index 000000000..bdc06c1ec --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/FileWithoutThumbnail/Incoming/FileWithoutThumbnailIncomingBubbleCell.swift @@ -0,0 +1,39 @@ +// +// 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 + +class FileWithoutThumbnailIncomingBubbleCell: FileWithoutThumbnailBaseBubbleCell { + + override func setupViews() { + super.setupViews() + + bubbleCellContentView?.showSenderInfo = true + + // TODO: Use constants + let messageViewMarginRight: CGFloat = 80 + let messageLeftMargin: CGFloat = 48 + + bubbleCellContentView?.innerContentViewTrailingConstraint.constant = messageViewMarginRight + bubbleCellContentView?.innerContentViewLeadingConstraint.constant = messageLeftMargin + } + + override func update(theme: Theme) { + super.update(theme: theme) + + self.fileAttachementView?.backgroundColor = theme.roomCellIncomingBubbleBackgroundColor + } +} diff --git a/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/FileWithoutThumbnail/Incoming/FileWithoutThumbnailIncomingWithPaginationTitleBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/FileWithoutThumbnail/Incoming/FileWithoutThumbnailIncomingWithPaginationTitleBubbleCell.swift new file mode 100644 index 000000000..518f4fc95 --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/FileWithoutThumbnail/Incoming/FileWithoutThumbnailIncomingWithPaginationTitleBubbleCell.swift @@ -0,0 +1,26 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit + +class FileWithoutThumbnailIncomingWithPaginationTitleBubbleCell: FileWithoutThumbnailIncomingBubbleCell { + + override func setupViews() { + super.setupViews() + + bubbleCellContentView?.showPaginationTitle = true + } +} diff --git a/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/FileWithoutThumbnail/Incoming/FileWithoutThumbnailIncomingWithoutSenderInfoBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/FileWithoutThumbnail/Incoming/FileWithoutThumbnailIncomingWithoutSenderInfoBubbleCell.swift new file mode 100644 index 000000000..1db1510ca --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/FileWithoutThumbnail/Incoming/FileWithoutThumbnailIncomingWithoutSenderInfoBubbleCell.swift @@ -0,0 +1,26 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit + +class FileWithoutThumbnailIncomingWithoutSenderInfoBubbleCell: FileWithoutThumbnailIncomingBubbleCell { + + override func setupViews() { + super.setupViews() + + bubbleCellContentView?.showSenderInfo = false + } +} diff --git a/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/FileWithoutThumbnail/Outgoing/FileWithoutThumbnailOutoingWithPaginationTitleBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/FileWithoutThumbnail/Outgoing/FileWithoutThumbnailOutoingWithPaginationTitleBubbleCell.swift new file mode 100644 index 000000000..0cf7acb6c --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/FileWithoutThumbnail/Outgoing/FileWithoutThumbnailOutoingWithPaginationTitleBubbleCell.swift @@ -0,0 +1,26 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import UIKit + +class FileWithoutThumbnailOutoingWithPaginationTitleBubbleCell: FileWithoutThumbnailOutoingWithoutSenderInfoBubbleCell { + + override func setupViews() { + super.setupViews() + + bubbleCellContentView?.showPaginationTitle = true + } +} diff --git a/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/FileWithoutThumbnail/Outgoing/FileWithoutThumbnailOutoingWithoutSenderInfoBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/FileWithoutThumbnail/Outgoing/FileWithoutThumbnailOutoingWithoutSenderInfoBubbleCell.swift new file mode 100644 index 000000000..67cb6ac3a --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/FileWithoutThumbnail/Outgoing/FileWithoutThumbnailOutoingWithoutSenderInfoBubbleCell.swift @@ -0,0 +1,40 @@ +// +// 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 + +class FileWithoutThumbnailOutoingWithoutSenderInfoBubbleCell: FileWithoutThumbnailBaseBubbleCell { + + override func setupViews() { + super.setupViews() + + bubbleCellContentView?.showSenderInfo = false + + // TODO: Use constants + // Same as outgoing message + let rightMargin: CGFloat = 34.0 + let leftMargin: CGFloat = 80.0 + + bubbleCellContentView?.innerContentViewTrailingConstraint.constant = rightMargin + bubbleCellContentView?.innerContentViewLeadingConstraint.constant = leftMargin + } + + override func update(theme: Theme) { + super.update(theme: theme) + + self.fileAttachementView?.backgroundColor = theme.roomCellOutgoingBubbleBackgroundColor + } +} diff --git a/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/Location/Incoming/LocationIncomingBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/Location/Incoming/LocationIncomingBubbleCell.swift new file mode 100644 index 000000000..b0818f550 --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/Location/Incoming/LocationIncomingBubbleCell.swift @@ -0,0 +1,31 @@ +// +// 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 LocationIncomingBubbleCell: LocationBubbleCell { + + override func setupViews() { + super.setupViews() + + // TODO: Use constants + let messageViewMarginRight: CGFloat = 80 + let messageLeftMargin: CGFloat = 48 + + bubbleCellContentView?.innerContentViewTrailingConstraint.constant = messageViewMarginRight + bubbleCellContentView?.innerContentViewLeadingConstraint.constant = messageLeftMargin + } +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionStateAction.swift b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/Location/Incoming/LocationIncomingWithPaginationTitleBubbleCell.swift similarity index 74% rename from RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionStateAction.swift rename to Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/Location/Incoming/LocationIncomingWithPaginationTitleBubbleCell.swift index f21630348..77b1e17e1 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionStateAction.swift +++ b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/Location/Incoming/LocationIncomingWithPaginationTitleBubbleCell.swift @@ -16,7 +16,10 @@ import Foundation -@available(iOS 14.0, *) -enum UserSuggestionStateAction { - case updateWithItems([UserSuggestionItemProtocol]) +class LocationIncomingWithPaginationTitleBubbleCell: LocationIncomingBubbleCell { + override func setupViews() { + super.setupViews() + + bubbleCellContentView?.showPaginationTitle = true + } } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorParameters.swift b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/Location/Incoming/LocationIncomingWithoutSenderInfoBubbleCell.swift similarity index 74% rename from RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorParameters.swift rename to Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/Location/Incoming/LocationIncomingWithoutSenderInfoBubbleCell.swift index bd9d54e8b..880e7c286 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorParameters.swift +++ b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/Location/Incoming/LocationIncomingWithoutSenderInfoBubbleCell.swift @@ -16,7 +16,10 @@ import Foundation -struct UserSuggestionCoordinatorParameters { - let mediaManager: MXMediaManager - let room: MXRoom +class LocationIncomingWithoutSenderInfoBubbleCell: LocationIncomingBubbleCell { + override func setupViews() { + super.setupViews() + + bubbleCellContentView?.showSenderInfo = false + } } diff --git a/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/Location/Outgoing/LocationOutgoingWithPaginationTitleBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/Location/Outgoing/LocationOutgoingWithPaginationTitleBubbleCell.swift new file mode 100644 index 000000000..f1158043f --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/Location/Outgoing/LocationOutgoingWithPaginationTitleBubbleCell.swift @@ -0,0 +1,25 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +class LocationOutgoingWithPaginationTitleBubbleCell: LocationOutgoingWithoutSenderInfoBubbleCell { + override func setupViews() { + super.setupViews() + + bubbleCellContentView?.showPaginationTitle = true + } +} diff --git a/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/Location/Outgoing/LocationOutgoingWithoutSenderInfoBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/Location/Outgoing/LocationOutgoingWithoutSenderInfoBubbleCell.swift new file mode 100644 index 000000000..8c46e21de --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/Location/Outgoing/LocationOutgoingWithoutSenderInfoBubbleCell.swift @@ -0,0 +1,33 @@ +// +// 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 LocationOutgoingWithoutSenderInfoBubbleCell: LocationBubbleCell { + override func setupViews() { + super.setupViews() + + bubbleCellContentView?.showSenderInfo = false + + // TODO: Use constants + // Same as outgoing message + let rightMargin: CGFloat = 34.0 + let leftMargin: CGFloat = 80.0 + + bubbleCellContentView?.innerContentViewTrailingConstraint.constant = rightMargin + bubbleCellContentView?.innerContentViewLeadingConstraint.constant = leftMargin + } +} diff --git a/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/VoiceMessage/Incoming/VoiceMessageIncomingBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/VoiceMessage/Incoming/VoiceMessageIncomingBubbleCell.swift new file mode 100644 index 000000000..f86756e9e --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/VoiceMessage/Incoming/VoiceMessageIncomingBubbleCell.swift @@ -0,0 +1,43 @@ +// +// 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 VoiceMessageIncomingBubbleCell: VoiceMessageBubbleCell { + + override func setupViews() { + super.setupViews() + + // TODO: Use constants + let messageViewMarginRight: CGFloat = 80 + let messageLeftMargin: CGFloat = 48 + let playbackViewRightMargin: CGFloat = 40 + + bubbleCellContentView?.innerContentViewTrailingConstraint.constant = messageViewMarginRight + bubbleCellContentView?.innerContentViewLeadingConstraint.constant = messageLeftMargin + + playbackController.playbackView.stackViewTrailingContraint.constant = playbackViewRightMargin + } + + override func update(theme: Theme) { + + guard let playbackController = playbackController else { + return + } + + playbackController.playbackView.customBackgroundViewColor = theme.roomCellIncomingBubbleBackgroundColor + } +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewModelResult.swift b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/VoiceMessage/Incoming/VoiceMessageIncomingWithPaginationTitleBubbleCell.swift similarity index 73% rename from RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewModelResult.swift rename to Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/VoiceMessage/Incoming/VoiceMessageIncomingWithPaginationTitleBubbleCell.swift index b15a983b6..9e8e6513f 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewModelResult.swift +++ b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/VoiceMessage/Incoming/VoiceMessageIncomingWithPaginationTitleBubbleCell.swift @@ -16,7 +16,10 @@ import Foundation -@available(iOS 14, *) -enum UserSuggestionViewModelResult { - case selectedItemWithIdentifier(String) +class VoiceMessageIncomingWithPaginationTitleBubbleCell: VoiceMessageIncomingBubbleCell { + override func setupViews() { + super.setupViews() + + bubbleCellContentView?.showPaginationTitle = true + } } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewAction.swift b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/VoiceMessage/Incoming/VoiceMessageIncomingWithoutSenderInfoBubbleCell.swift similarity index 73% rename from RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewAction.swift rename to Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/VoiceMessage/Incoming/VoiceMessageIncomingWithoutSenderInfoBubbleCell.swift index d08fa62ec..f0d5385e6 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewAction.swift +++ b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/VoiceMessage/Incoming/VoiceMessageIncomingWithoutSenderInfoBubbleCell.swift @@ -16,7 +16,10 @@ import Foundation -@available(iOS 14, *) -enum UserSuggestionViewAction { - case selectedItem(UserSuggestionViewStateItem) +class VoiceMessageIncomingWithoutSenderInfoBubbleCell: VoiceMessageIncomingBubbleCell { + override func setupViews() { + super.setupViews() + + bubbleCellContentView?.showSenderInfo = false + } } diff --git a/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/VoiceMessage/Outgoing/VoiceMessageOutgoingWithPaginationTitleBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/VoiceMessage/Outgoing/VoiceMessageOutgoingWithPaginationTitleBubbleCell.swift new file mode 100644 index 000000000..9f77e7843 --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/VoiceMessage/Outgoing/VoiceMessageOutgoingWithPaginationTitleBubbleCell.swift @@ -0,0 +1,26 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +class VoiceMessageOutgoingWithPaginationTitleBubbleCell: VoiceMessageOutgoingWithoutSenderInfoBubbleCell { + + override func setupViews() { + super.setupViews() + + bubbleCellContentView?.showPaginationTitle = true + } +} diff --git a/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/VoiceMessage/Outgoing/VoiceMessageOutgoingWithoutSenderInfoBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/VoiceMessage/Outgoing/VoiceMessageOutgoingWithoutSenderInfoBubbleCell.swift new file mode 100644 index 000000000..1b18d861c --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/Styles/Bubble/Cells/VoiceMessage/Outgoing/VoiceMessageOutgoingWithoutSenderInfoBubbleCell.swift @@ -0,0 +1,46 @@ +// +// 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 VoiceMessageOutgoingWithoutSenderInfoBubbleCell: VoiceMessageBubbleCell { + + override func setupViews() { + super.setupViews() + + bubbleCellContentView?.showSenderInfo = false + + // TODO: Use constants + // Same as outgoing message + let rightMargin: CGFloat = 34.0 + let leftMargin: CGFloat = 80.0 + let playbackViewRightMargin: CGFloat = 40 + + bubbleCellContentView?.innerContentViewTrailingConstraint.constant = rightMargin + bubbleCellContentView?.innerContentViewLeadingConstraint.constant = leftMargin + + playbackController.playbackView.stackViewTrailingContraint.constant = playbackViewRightMargin + } + + override func update(theme: Theme) { + + guard let playbackController = playbackController else { + return + } + + playbackController.playbackView.customBackgroundViewColor = theme.roomCellOutgoingBubbleBackgroundColor + } +} diff --git a/Riot/Modules/Room/Views/BubbleCells/Styles/Plain/PlainRoomTimelineCellDecorator.swift b/Riot/Modules/Room/Views/BubbleCells/Styles/Plain/PlainRoomTimelineCellDecorator.swift index 749ccece5..a4e4bb8ae 100644 --- a/Riot/Modules/Room/Views/BubbleCells/Styles/Plain/PlainRoomTimelineCellDecorator.swift +++ b/Riot/Modules/Room/Views/BubbleCells/Styles/Plain/PlainRoomTimelineCellDecorator.swift @@ -19,6 +19,15 @@ import UIKit @objcMembers class PlainRoomTimelineCellDecorator: RoomTimelineCellDecorator { + // MARK: - Properties + + // TODO: Conforms to Themable and don't use ThemeService + var theme: Theme { + return ThemeService.shared().theme + } + + // MARK: - RoomTimelineCellDecorator + func addTimestampLabelIfNeeded(toCell cell: MXKRoomBubbleTableViewCell, cellData: RoomBubbleCellData) { guard cellData.containsLastMessage && cellData.isCollapsableAndCollapsed == false else { diff --git a/Riot/Modules/Room/Views/BubbleCells/Styles/Plain/PlainRoomTimelineCellProvider.h b/Riot/Modules/Room/Views/BubbleCells/Styles/Plain/PlainRoomTimelineCellProvider.h index 12b1876ee..8b51f9dea 100644 --- a/Riot/Modules/Room/Views/BubbleCells/Styles/Plain/PlainRoomTimelineCellProvider.h +++ b/Riot/Modules/Room/Views/BubbleCells/Styles/Plain/PlainRoomTimelineCellProvider.h @@ -20,10 +20,26 @@ NS_ASSUME_NONNULL_BEGIN @interface PlainRoomTimelineCellProvider: NSObject +#pragma mark - Registration + +- (void)registerVoiceMessageCellsForTableView:(UITableView*)tableView; + +- (void)registerLocationCellsForTableView:(UITableView*)tableView; + +#pragma mark - Mapping + - (NSDictionary*)outgoingTextMessageCellsMapping; - (NSDictionary*)outgoingAttachmentCellsMapping; +- (NSDictionary*)incomingAttachmentWithoutThumbnailCellsMapping; + +- (NSDictionary*)outgoingAttachmentWithoutThumbnailCellsMapping; + +- (NSDictionary*)voiceMessageCellsMapping; + +- (NSDictionary*)locationCellsMapping; + @end NS_ASSUME_NONNULL_END diff --git a/Riot/Modules/Room/Views/BubbleCells/Styles/Plain/PlainRoomTimelineCellProvider.m b/Riot/Modules/Room/Views/BubbleCells/Styles/Plain/PlainRoomTimelineCellProvider.m index e36abb851..b80dfcc74 100644 --- a/Riot/Modules/Room/Views/BubbleCells/Styles/Plain/PlainRoomTimelineCellProvider.m +++ b/Riot/Modules/Room/Views/BubbleCells/Styles/Plain/PlainRoomTimelineCellProvider.m @@ -283,6 +283,12 @@ NSDictionary *outgoingAttachmentCellsMapping = [self outgoingAttachmentCellsMapping]; [cellClasses addEntriesFromDictionary:outgoingAttachmentCellsMapping]; + NSDictionary *outgoingAttachmentWithoutThumbnailCellsMapping = [self outgoingAttachmentWithoutThumbnailCellsMapping]; + [cellClasses addEntriesFromDictionary:outgoingAttachmentWithoutThumbnailCellsMapping]; + + NSDictionary *incomingAttachmentWithoutThumbnailCellsMapping = [self incomingAttachmentWithoutThumbnailCellsMapping]; + [cellClasses addEntriesFromDictionary:incomingAttachmentWithoutThumbnailCellsMapping]; + // Other cells NSDictionary *roomMembershipCellsMapping = [self membershipCellsMapping]; @@ -382,6 +388,34 @@ }; } +- (NSDictionary*)incomingAttachmentWithoutThumbnailCellsMapping +{ + return @{ + // Clear + @(RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnail) : RoomIncomingTextMsgBubbleCell.class, + @(RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnailWithoutSenderInfo) : RoomIncomingTextMsgWithoutSenderInfoBubbleCell.class, + @(RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnailWithPaginationTitle) : RoomIncomingTextMsgWithPaginationTitleBubbleCell.class, + // Encrypted + @(RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnailEncrypted) : RoomIncomingEncryptedTextMsgBubbleCell.class, + @(RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnailEncryptedWithoutSenderInfo) : RoomIncomingEncryptedTextMsgWithoutSenderInfoBubbleCell.class, + @(RoomTimelineCellIdentifierIncomingAttachmentWithoutThumbnailEncryptedWithPaginationTitle) : RoomIncomingEncryptedTextMsgWithPaginationTitleBubbleCell.class + }; +} + +- (NSDictionary*)outgoingAttachmentWithoutThumbnailCellsMapping +{ + return @{ + // Clear + @(RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnail) : RoomOutgoingTextMsgBubbleCell.class, + @(RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnailWithoutSenderInfo) : RoomOutgoingTextMsgWithoutSenderInfoBubbleCell.class, + @(RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnailWithPaginationTitle) : RoomOutgoingTextMsgWithPaginationTitleBubbleCell.class, + // Encrypted + @(RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnailEncrypted) : RoomOutgoingEncryptedTextMsgBubbleCell.class, + @(RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnailEncryptedWithoutSenderInfo) : RoomOutgoingEncryptedTextMsgWithoutSenderInfoBubbleCell.class, + @(RoomTimelineCellIdentifierOutgoingAttachmentWithoutThumbnailEncryptedWithPaginationTitle) : RoomOutgoingEncryptedTextMsgWithPaginationTitleBubbleCell.class + }; +} + - (NSDictionary*)membershipCellsMapping { return @{ @@ -425,9 +459,14 @@ - (NSDictionary*)voiceMessageCellsMapping { return @{ - @(RoomTimelineCellIdentifierVoiceMessage) : VoiceMessageBubbleCell.class, - @(RoomTimelineCellIdentifierVoiceMessageWithoutSenderInfo) : VoiceMessageWithoutSenderInfoBubbleCell.class, - @(RoomTimelineCellIdentifierVoiceMessageWithPaginationTitle) : VoiceMessageWithPaginationTitleBubbleCell.class, + // Incoming + @(RoomTimelineCellIdentifierIncomingVoiceMessage) : VoiceMessageBubbleCell.class, + @(RoomTimelineCellIdentifierIncomingVoiceMessageWithoutSenderInfo) : VoiceMessageWithoutSenderInfoBubbleCell.class, + @(RoomTimelineCellIdentifierIncomingVoiceMessageWithPaginationTitle) : VoiceMessageWithPaginationTitleBubbleCell.class, + // Outoing + @(RoomTimelineCellIdentifierOutgoingVoiceMessage) : VoiceMessageBubbleCell.class, + @(RoomTimelineCellIdentifierOutgoingVoiceMessageWithoutSenderInfo) : VoiceMessageWithoutSenderInfoBubbleCell.class, + @(RoomTimelineCellIdentifierOutgoingVoiceMessageWithPaginationTitle) : VoiceMessageWithPaginationTitleBubbleCell.class }; } @@ -443,9 +482,14 @@ - (NSDictionary*)locationCellsMapping { return @{ - @(RoomTimelineCellIdentifierLocation) : LocationBubbleCell.class, - @(RoomTimelineCellIdentifierLocationWithoutSenderInfo) : LocationWithoutSenderInfoBubbleCell.class, - @(RoomTimelineCellIdentifierLocationWithPaginationTitle) : LocationWithPaginationTitleBubbleCell.class + // Incoming + @(RoomTimelineCellIdentifierIncomingLocation) : LocationBubbleCell.class, + @(RoomTimelineCellIdentifierIncomingLocationWithoutSenderInfo) : LocationWithoutSenderInfoBubbleCell.class, + @(RoomTimelineCellIdentifierIncomingLocationWithPaginationTitle) : LocationWithPaginationTitleBubbleCell.class, + // Outgoing + @(RoomTimelineCellIdentifierOutgoingLocation) : LocationBubbleCell.class, + @(RoomTimelineCellIdentifierOutgoingLocationWithoutSenderInfo) : LocationWithoutSenderInfoBubbleCell.class, + @(RoomTimelineCellIdentifierOutgoingLocationWithPaginationTitle) : LocationWithPaginationTitleBubbleCell.class }; } diff --git a/Riot/Modules/Room/Views/BubbleCells/Styles/RoomCellLayoutUpdating.swift b/Riot/Modules/Room/Views/BubbleCells/Styles/RoomCellLayoutUpdating.swift index aaacccb6f..78f5330f1 100644 --- a/Riot/Modules/Room/Views/BubbleCells/Styles/RoomCellLayoutUpdating.swift +++ b/Riot/Modules/Room/Views/BubbleCells/Styles/RoomCellLayoutUpdating.swift @@ -27,4 +27,6 @@ protocol RoomCellLayoutUpdating: Themable { func setupLayout(forOutgoingTextMessageCell cell: MXKRoomBubbleTableViewCell) func setupLayout(forOutgoingFileAttachmentCell cell: MXKRoomBubbleTableViewCell) + + func updateLayout(forSelectedStickerCell cell: RoomSelectedStickerBubbleCell) } diff --git a/Riot/Modules/Room/Views/BubbleCells/VoiceMessage/VoiceMessageBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/VoiceMessage/VoiceMessageBubbleCell.swift index a389e3eb2..55c7f2487 100644 --- a/Riot/Modules/Room/Views/BubbleCells/VoiceMessage/VoiceMessageBubbleCell.swift +++ b/Riot/Modules/Room/Views/BubbleCells/VoiceMessage/VoiceMessageBubbleCell.swift @@ -18,7 +18,7 @@ import Foundation class VoiceMessageBubbleCell: SizableBaseBubbleCell, BubbleCellReactionsDisplayable { - private var playbackController: VoiceMessagePlaybackController! + private(set) var playbackController: VoiceMessagePlaybackController! override func render(_ cellData: MXKCellData!) { super.render(cellData) @@ -27,13 +27,15 @@ class VoiceMessageBubbleCell: SizableBaseBubbleCell, BubbleCellReactionsDisplaya return } - guard data.attachment.type == MXKAttachmentTypeVoiceMessage || data.attachment.type == MXKAttachmentTypeAudio else { + guard data.attachment.type == .voiceMessage || data.attachment.type == .audio else { fatalError("Invalid attachment type passed to a voice message cell.") } if playbackController.attachment != data.attachment { playbackController.attachment = data.attachment } + + self.update(theme: ThemeService.shared().theme) } override func setupViews() { @@ -52,4 +54,15 @@ class VoiceMessageBubbleCell: SizableBaseBubbleCell, BubbleCellReactionsDisplaya contentView.vc_addSubViewMatchingParent(playbackController.playbackView) } + + override func update(theme: Theme) { + + super.update(theme: theme) + + guard let playbackController = playbackController else { + return + } + + playbackController.playbackView.update(theme: theme) + } } diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift index 14d466af6..6c6200298 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessageAttachmentCacheManager.swift @@ -85,7 +85,7 @@ class VoiceMessageAttachmentCacheManager { } func loadAttachment(_ attachment: MXKAttachment, numberOfSamples: Int, completion: @escaping (Result) -> Void) { - guard attachment.type == MXKAttachmentTypeVoiceMessage || attachment.type == MXKAttachmentTypeAudio else { + guard attachment.type == .voiceMessage || attachment.type == .audio else { completion(Result.failure(VoiceMessageAttachmentCacheManagerError.invalidAttachmentType)) MXLog.error("[VoiceMessageAttachmentCacheManager] Invalid attachment type, ignoring request.") return diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift index 8c13de2d3..c3abeb01f 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.swift @@ -49,6 +49,7 @@ class VoiceMessagePlaybackView: UIView, NibLoadable, Themable { @IBOutlet private var playButton: UIButton! @IBOutlet private var elapsedTimeLabel: UILabel! @IBOutlet private var waveformContainerView: UIView! + @IBOutlet private (set)var stackViewTrailingContraint: NSLayoutConstraint! private var longPressGestureRecognizer: UILongPressGestureRecognizer! private var panGestureRecognizer: UIPanGestureRecognizer! @@ -61,6 +62,16 @@ class VoiceMessagePlaybackView: UIView, NibLoadable, Themable { return _waveformView } + /// Define the `backgroundView.backgroundColor`. + /// By setting this value the theme color will not be applyied to `backgroundView` in `update(theme: Theme)` method. + var customBackgroundViewColor: UIColor? { + didSet { + if let theme = currentTheme { + self.update(theme: theme) + } + } + } + override var bounds: CGRect { didSet { if oldValue.width != bounds.width { @@ -128,7 +139,10 @@ class VoiceMessagePlaybackView: UIView, NibLoadable, Themable { self.backgroundColor = theme.colors.background playButton.backgroundColor = theme.colors.background playButton.tintColor = theme.colors.secondaryContent - backgroundView.backgroundColor = theme.colors.quinaryContent + + let backgroundViewColor = self.customBackgroundViewColor ?? theme.colors.quinaryContent + + backgroundView.backgroundColor = backgroundViewColor _waveformView.primaryLineColor = theme.colors.quarterlyContent _waveformView.secondaryLineColor = theme.colors.secondaryContent elapsedTimeLabel.textColor = theme.colors.tertiaryContent diff --git a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.xib b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.xib index 1bbca0184..943832e99 100644 --- a/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.xib +++ b/Riot/Modules/Room/VoiceMessages/VoiceMessagePlaybackView.xib @@ -1,9 +1,9 @@ - + - + @@ -78,6 +78,7 @@ + diff --git a/Riot/Modules/Settings/SettingsViewController.m b/Riot/Modules/Settings/SettingsViewController.m index 4adfd5a27..d009b7b6e 100644 --- a/Riot/Modules/Settings/SettingsViewController.m +++ b/Riot/Modules/Settings/SettingsViewController.m @@ -51,7 +51,6 @@ typedef NS_ENUM(NSUInteger, SECTION_TAG) { SECTION_TAG_SIGN_OUT = 0, SECTION_TAG_USER_SETTINGS, - SECTION_TAG_LOCATION_SHARING, SECTION_TAG_SENDING_MEDIA, SECTION_TAG_LINKS, SECTION_TAG_SECURITY, @@ -87,11 +86,6 @@ typedef NS_ENUM(NSUInteger, USER_SETTINGS_OFFSET) USER_SETTINGS_PHONENUMBERS_OFFSET = 1000 }; -typedef NS_ENUM(NSUInteger, LOCATION_SHARING) -{ - LOCATION_SHARING_ENABLED -}; - typedef NS_ENUM(NSUInteger, SENDING_MEDIA) { SENDING_MEDIA_CONFIRM_SIZE = 0 @@ -381,15 +375,7 @@ TableViewSectionsDelegate> sectionUserSettings.headerTitle = [VectorL10n settingsUserSettings]; [tmpSections addObject:sectionUserSettings]; - - if (BuildSettings.locationSharingEnabled) - { - Section *sectionLocationSharing = [Section sectionWithTag:SECTION_TAG_LOCATION_SHARING]; - [sectionLocationSharing addRowWithTag:LOCATION_SHARING_ENABLED]; - sectionLocationSharing.headerTitle = VectorL10n.locationSharingSettingsHeader.uppercaseString; - [tmpSections addObject:sectionLocationSharing]; - } - + if (BuildSettings.settingsScreenShowConfirmMediaSize) { Section *sectionMedia = [Section sectionWithTag:SECTION_TAG_SENDING_MEDIA]; @@ -1964,21 +1950,6 @@ TableViewSectionsDelegate> cell = passwordCell; } } - else if (section == SECTION_TAG_LOCATION_SHARING) - { - if (row == LOCATION_SHARING_ENABLED) - { - MXKTableViewCellWithLabelAndSwitch* labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath]; - - labelAndSwitchCell.mxkLabel.text = VectorL10n.locationSharingSettingsToggleTitle; - labelAndSwitchCell.mxkSwitch.on = RiotSettings.shared.roomScreenAllowLocationAction; - labelAndSwitchCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor; - labelAndSwitchCell.mxkSwitch.enabled = YES; - [labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleLocationSharing:) forControlEvents:UIControlEventTouchUpInside]; - - cell = labelAndSwitchCell; - } - } else if (section == SECTION_TAG_SENDING_MEDIA) { if (row == SENDING_MEDIA_CONFIRM_SIZE) @@ -3013,11 +2984,6 @@ TableViewSectionsDelegate> } } -- (void)toggleLocationSharing:(UISwitch *)sender -{ - RiotSettings.shared.roomScreenAllowLocationAction = sender.on; -} - - (void)toggleConfirmMediaSize:(UISwitch *)sender { RiotSettings.shared.showMediaCompressionPrompt = sender.on; diff --git a/Riot/Modules/Spaces/SpaceDetail/SpaceDetailViewController.swift b/Riot/Modules/Spaces/SpaceDetail/SpaceDetailViewController.swift index bca282949..60c942176 100644 --- a/Riot/Modules/Spaces/SpaceDetail/SpaceDetailViewController.swift +++ b/Riot/Modules/Spaces/SpaceDetail/SpaceDetailViewController.swift @@ -186,7 +186,7 @@ class SpaceDetailViewController: UIViewController { private func setup(button: UIButton, withTitle title: String) { button.layer.masksToBounds = true button.layer.cornerRadius = 8.0 - button.setTitle(title.uppercased(), for: .normal) + button.setTitle(title, for: .normal) } private func render(viewState: SpaceDetailViewState) { diff --git a/Riot/SupportingFiles/Riot-Bridging-Header.h b/Riot/SupportingFiles/Riot-Bridging-Header.h index 9eaae3a43..165321909 100644 --- a/Riot/SupportingFiles/Riot-Bridging-Header.h +++ b/Riot/SupportingFiles/Riot-Bridging-Header.h @@ -49,6 +49,7 @@ #import "RoomTimelineCellProvider.h" #import "PlainRoomTimelineCellProvider.h" #import "BubbleRoomTimelineCellProvider.h" +#import "RoomSelectedStickerBubbleCell.h" // MatrixKit common imports, shared with all targets #import "MatrixKit-Bridging-Header.h" diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptModels.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptModels.swift index 99f96bce8..2cfdb6e0f 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptModels.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptModels.swift @@ -16,9 +16,6 @@ import Foundation -// The state is never modified so this is unnecessary. -enum AnalyticsPromptStateAction { } - enum AnalyticsPromptViewAction { /// Enable analytics. case enable diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptViewModel.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptViewModel.swift index 971929ab4..7cd852f71 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptViewModel.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptViewModel.swift @@ -19,7 +19,7 @@ import Combine @available(iOS 14, *) typealias AnalyticsPromptViewModelType = StateStoreViewModel @available(iOS 14, *) class AnalyticsPromptViewModel: AnalyticsPromptViewModelType { @@ -54,10 +54,6 @@ class AnalyticsPromptViewModel: AnalyticsPromptViewModelType { openTermsURL() } } - - override class func reducer(state: inout AnalyticsPromptViewState, action: AnalyticsPromptStateAction) { - // There is no mutable state to reduce :) - } /// Enable analytics. The call to the Analytics class is made in the completion. private func enable() { diff --git a/RiotSwiftUI/Modules/Common/ViewModel/StateStoreViewModel.swift b/RiotSwiftUI/Modules/Common/ViewModel/StateStoreViewModel.swift index 915859165..efde322fb 100644 --- a/RiotSwiftUI/Modules/Common/ViewModel/StateStoreViewModel.swift +++ b/RiotSwiftUI/Modules/Common/ViewModel/StateStoreViewModel.swift @@ -92,9 +92,9 @@ class StateStoreViewModel { /// Constrained interface for passing to Views. var context: Context - /// State can be read within the 'ViewModel' but not modified outside of the reducer. var state: State { - context.viewState + get { context.viewState } + set { context.viewState = newValue } } // MARK: Setup @@ -110,12 +110,14 @@ class StateStoreViewModel { /// Send state actions to modify the state within the reducer. /// - Parameter action: The state action to send to the reducer. + @available(*, deprecated, message: "Mutate state directly instead") func dispatch(action: StateAction) { Self.reducer(state: &context.viewState, action: action) } /// Send state actions from a publisher to modify the state within the reducer. /// - Parameter actionPublisher: The publisher that produces actions to be sent to the reducer + @available(*, deprecated, message: "Mutate state directly instead") func dispatch(actionPublisher: AnyPublisher) { actionPublisher.sink { [weak self] action in guard let self = self else { return } diff --git a/RiotSwiftUI/Modules/Onboarding/SplashScreen/OnboardingSplashScreenModels.swift b/RiotSwiftUI/Modules/Onboarding/SplashScreen/OnboardingSplashScreenModels.swift index 73f7f3ef4..d24b7679d 100644 --- a/RiotSwiftUI/Modules/Onboarding/SplashScreen/OnboardingSplashScreenModels.swift +++ b/RiotSwiftUI/Modules/Onboarding/SplashScreen/OnboardingSplashScreenModels.swift @@ -30,10 +30,6 @@ struct OnboardingSplashScreenPageContent { // MARK: View model -enum OnboardingSplashScreenStateAction { - case viewAction(OnboardingSplashScreenViewAction) -} - enum OnboardingSplashScreenViewModelResult { case register case login diff --git a/RiotSwiftUI/Modules/Onboarding/SplashScreen/OnboardingSplashScreenViewModel.swift b/RiotSwiftUI/Modules/Onboarding/SplashScreen/OnboardingSplashScreenViewModel.swift index f013bda1c..c51b843dc 100644 --- a/RiotSwiftUI/Modules/Onboarding/SplashScreen/OnboardingSplashScreenViewModel.swift +++ b/RiotSwiftUI/Modules/Onboarding/SplashScreen/OnboardingSplashScreenViewModel.swift @@ -19,7 +19,7 @@ import Combine @available(iOS 14, *) typealias OnboardingSplashScreenViewModelType = StateStoreViewModel protocol OnboardingSplashScreenViewModelProtocol { @@ -54,31 +54,18 @@ class OnboardingSplashScreenViewModel: OnboardingSplashScreenViewModelType, Onbo register() case .login: login() - case .nextPage, .previousPage, .hiddenPage: - dispatch(action: .viewAction(viewAction)) + case .nextPage: + // Wrap back round to the first page index when reaching the end. + state.bindings.pageIndex = (state.bindings.pageIndex + 1) % state.content.count + case .previousPage: + // Prevent the hidden page at index -1 from being shown. + state.bindings.pageIndex = max(0, (state.bindings.pageIndex - 1)) + case .hiddenPage: + // Hidden page for a nicer animation when looping back to the start. + state.bindings.pageIndex = -1 } } - override class func reducer(state: inout OnboardingSplashScreenViewState, action: OnboardingSplashScreenStateAction) { - switch action { - case .viewAction(let viewAction): - switch viewAction { - case .nextPage: - // Wrap back round to the first page index when reaching the end. - state.bindings.pageIndex = (state.bindings.pageIndex + 1) % state.content.count - case .previousPage: - // Prevent the hidden page at index -1 from being shown. - state.bindings.pageIndex = max(0, (state.bindings.pageIndex - 1)) - case .hiddenPage: - // Hidden page for a nicer animation when looping back to the start. - state.bindings.pageIndex = -1 - case .login, .register: - break - } - } - UILog.debug("[OnboardingSplashScreenViewModel] reducer with action \(action) produced state: \(state)") - } - private func register() { completion?(.register) } diff --git a/RiotSwiftUI/Modules/Onboarding/SplashScreen/Test/Unit/OnboardingSplashScreenViewModelTests.swift b/RiotSwiftUI/Modules/Onboarding/SplashScreen/Test/Unit/OnboardingSplashScreenViewModelTests.swift index 78bb372d4..c247b4027 100644 --- a/RiotSwiftUI/Modules/Onboarding/SplashScreen/Test/Unit/OnboardingSplashScreenViewModelTests.swift +++ b/RiotSwiftUI/Modules/Onboarding/SplashScreen/Test/Unit/OnboardingSplashScreenViewModelTests.swift @@ -21,5 +21,5 @@ import Combine @available(iOS 14.0, *) class OnboardingSplashScreenViewModelTests: XCTestCase { - // TODO: Check for any useful tests when finished + } diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift b/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift index 471187dd5..bb8de9124 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/Coordinator/LocationSharingCoordinator.swift @@ -33,12 +33,7 @@ final class LocationSharingCoordinator: Coordinator, Presentable { private let parameters: LocationSharingCoordinatorParameters private let locationSharingHostingController: UIViewController - private var _locationSharingViewModel: Any? = nil - - @available(iOS 14.0, *) - fileprivate var locationSharingViewModel: LocationSharingViewModel { - return _locationSharingViewModel as! LocationSharingViewModel - } + private var locationSharingViewModel: LocationSharingViewModelProtocol // MARK: Public @@ -58,7 +53,7 @@ final class LocationSharingCoordinator: Coordinator, Presentable { let view = LocationSharingView(context: viewModel.context) .addDependency(AvatarService.instantiate(mediaManager: parameters.mediaManager)) - _locationSharingViewModel = viewModel + locationSharingViewModel = viewModel locationSharingHostingController = VectorHostingController(rootView: view) } @@ -81,20 +76,18 @@ final class LocationSharingCoordinator: Coordinator, Presentable { return } - self.locationSharingViewModel.dispatch(action: .startLoading) + self.locationSharingViewModel.startLoading() - self.parameters.roomDataSource.sendLocation(withLatitude: latitude, - longitude: longitude, - description: nil) { [weak self] _ in + self.parameters.roomDataSource.sendLocation(withLatitude: latitude, longitude: longitude, description: nil) { [weak self] _ in guard let self = self else { return } - self.locationSharingViewModel.dispatch(action: .stopLoading(nil)) + self.locationSharingViewModel.stopLoading() self.completion?() } failure: { [weak self] error in guard let self = self else { return } MXLog.error("[LocationSharingCoordinator] Failed sharing location with error: \(String(describing: error))") - self.locationSharingViewModel.dispatch(action: .stopLoading(error)) + self.locationSharingViewModel.stopLoading(error: .locationSharingError) } } diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift index 88b0ee9cb..f9b188c81 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift @@ -19,31 +19,23 @@ import SwiftUI import Combine import CoreLocation -enum LocationSharingViewError { - case failedLoadingMap - case failedLocatingUser - case invalidLocationAuthorization - case failedSharingLocation -} - -enum LocationSharingStateAction { - case error(LocationSharingViewError, LocationSharingViewModelCallback?) - case startLoading - case stopLoading(Error?) -} - enum LocationSharingViewAction { case cancel case share } -typealias LocationSharingViewModelCallback = ((LocationSharingViewModelResult) -> Void) - enum LocationSharingViewModelResult { case cancel case share(latitude: Double, longitude: Double) } +enum LocationSharingViewError { + case failedLoadingMap + case failedLocatingUser + case invalidLocationAuthorization + case failedSharingLocation +} + @available(iOS 14, *) struct LocationSharingViewState: BindableState { let tileServerMapURL: URL @@ -80,6 +72,7 @@ struct LocationSharingErrorAlertInfo: Identifiable { let id: AlertType let title: String + var subtitle: String? = nil let primaryButton: (title: String, action: (() -> Void)?) - let secondaryButton: (title: String, action: (() -> Void)?)? + var secondaryButton: (title: String, action: (() -> Void)?)? = nil } diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift index 40b750f85..821b525bb 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift @@ -19,11 +19,11 @@ import Combine import CoreLocation @available(iOS 14, *) -typealias LocationSharingViewModelType = StateStoreViewModel< LocationSharingViewState, - LocationSharingStateAction, - LocationSharingViewAction > +typealias LocationSharingViewModelType = StateStoreViewModel @available(iOS 14, *) -class LocationSharingViewModel: LocationSharingViewModelType { +class LocationSharingViewModel: LocationSharingViewModelType, LocationSharingViewModelProtocol { // MARK: - Properties @@ -41,7 +41,7 @@ class LocationSharingViewModel: LocationSharingViewModelType { state.errorSubject.sink { [weak self] error in guard let self = self else { return } - self.dispatch(action: .error(error, self.completion)) + self.processError(error) }.store(in: &cancellables) } @@ -58,7 +58,7 @@ class LocationSharingViewModel: LocationSharingViewModelType { } guard let location = state.bindings.userLocation else { - dispatch(action: .error(.failedLocatingUser, completion)) + processError(.failedLocatingUser) return } @@ -66,45 +66,54 @@ class LocationSharingViewModel: LocationSharingViewModelType { } } - override class func reducer(state: inout LocationSharingViewState, action: LocationSharingStateAction) { - switch action { - case .error(let error, let completion): - - switch error { - case .failedLoadingMap: - state.bindings.alertInfo = LocationSharingErrorAlertInfo(id: .mapLoadingError, - title: VectorL10n.locationSharingLoadingMapErrorTitle(AppInfo.current.displayName) , - primaryButton: (VectorL10n.ok, { completion?(.cancel) }), - secondaryButton: nil) - case .failedLocatingUser: - state.bindings.alertInfo = LocationSharingErrorAlertInfo(id: .userLocatingError, - title: VectorL10n.locationSharingLocatingUserErrorTitle(AppInfo.current.displayName), - primaryButton: (VectorL10n.ok, { completion?(.cancel) }), - secondaryButton: nil) - case .invalidLocationAuthorization: - state.bindings.alertInfo = LocationSharingErrorAlertInfo(id: .authorizationError, - title: VectorL10n.locationSharingInvalidAuthorizationErrorTitle(AppInfo.current.displayName), - primaryButton: (VectorL10n.locationSharingInvalidAuthorizationNotNow, { completion?(.cancel) }), - secondaryButton: (VectorL10n.locationSharingInvalidAuthorizationSettings, { - if let applicationSettingsURL = URL(string:UIApplication.openSettingsURLString) { - UIApplication.shared.open(applicationSettingsURL) - } - })) - default: - break - } - - case .startLoading: - state.showLoadingIndicator = true - case .stopLoading(let error): - state.showLoadingIndicator = false - - if error != nil { - state.bindings.alertInfo = LocationSharingErrorAlertInfo(id: .locationSharingError, - title: VectorL10n.locationSharingInvalidAuthorizationErrorTitle(AppInfo.current.displayName), - primaryButton: (VectorL10n.ok, nil), - secondaryButton: nil) - } + // MARK: - LocationSharingViewModelProtocol + + public func startLoading() { + state.showLoadingIndicator = true + } + + func stopLoading(error: LocationSharingErrorAlertInfo.AlertType?) { + state.showLoadingIndicator = false + + if let error = error { + state.bindings.alertInfo = LocationSharingErrorAlertInfo(id: error, + title: VectorL10n.locationSharingPostFailureTitle, + subtitle: VectorL10n.locationSharingPostFailureSubtitle(AppInfo.current.displayName), + primaryButton: (VectorL10n.ok, nil)) + } + } + + // MARK: - Private + + private func processError(_ error: LocationSharingViewError) { + guard state.bindings.alertInfo == nil else { + return + } + + let primaryButtonCompletion = { [weak self] () -> Void in + self?.completion?(.cancel) + } + + switch error { + case .failedLoadingMap: + state.bindings.alertInfo = LocationSharingErrorAlertInfo(id: .mapLoadingError, + title: VectorL10n.locationSharingLoadingMapErrorTitle(AppInfo.current.displayName), + primaryButton: (VectorL10n.ok, primaryButtonCompletion)) + case .failedLocatingUser: + state.bindings.alertInfo = LocationSharingErrorAlertInfo(id: .userLocatingError, + title: VectorL10n.locationSharingLocatingUserErrorTitle(AppInfo.current.displayName), + primaryButton: (VectorL10n.ok, primaryButtonCompletion)) + case .invalidLocationAuthorization: + state.bindings.alertInfo = LocationSharingErrorAlertInfo(id: .authorizationError, + title: VectorL10n.locationSharingInvalidAuthorizationErrorTitle(AppInfo.current.displayName), + primaryButton: (VectorL10n.locationSharingInvalidAuthorizationNotNow, primaryButtonCompletion), + secondaryButton: (VectorL10n.locationSharingInvalidAuthorizationSettings, { + if let applicationSettingsURL = URL(string:UIApplication.openSettingsURLString) { + UIApplication.shared.open(applicationSettingsURL) + } + })) + default: + break } } } diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModelProtocol.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModelProtocol.swift new file mode 100644 index 000000000..38fbde4d4 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModelProtocol.swift @@ -0,0 +1,30 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +protocol LocationSharingViewModelProtocol { + var completion: ((LocationSharingViewModelResult) -> Void)? { get set } + + func startLoading() + func stopLoading(error: LocationSharingErrorAlertInfo.AlertType?) +} + +extension LocationSharingViewModelProtocol { + func stopLoading() { + stopLoading(error: nil) + } +} diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/Test/UI/LocationSharingUITests.swift b/RiotSwiftUI/Modules/Room/LocationSharing/Test/UI/LocationSharingUITests.swift index 6e68f3556..d5040db2f 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/Test/UI/LocationSharingUITests.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/Test/UI/LocationSharingUITests.swift @@ -41,7 +41,7 @@ class LocationSharingUITests: XCTestCase { goToScreenWithIdentifier(MockLocationSharingScreenState.displayExistingLocation.title) XCTAssertTrue(app.buttons["Cancel"].exists) - XCTAssertTrue(app.buttons["location share icon"].exists) + XCTAssertTrue(app.buttons["LocationSharingView.shareButton"].exists) XCTAssertTrue(app.otherElements["Map"].exists) } diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/Test/Unit/LocationSharingViewModelTests.swift b/RiotSwiftUI/Modules/Room/LocationSharing/Test/Unit/LocationSharingViewModelTests.swift index e3371727c..c091b1817 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/Test/Unit/LocationSharingViewModelTests.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/Test/Unit/LocationSharingViewModelTests.swift @@ -100,12 +100,12 @@ class LocationSharingViewModelTests: XCTestCase { func testLoading() { let viewModel = buildViewModel(withLocation: false) - viewModel.dispatch(action: .startLoading) + viewModel.startLoading() XCTAssertFalse(viewModel.context.viewState.shareButtonEnabled) XCTAssertTrue(viewModel.context.viewState.showLoadingIndicator) - viewModel.dispatch(action: .stopLoading(nil)) + viewModel.stopLoading() XCTAssertTrue(viewModel.context.viewState.shareButtonEnabled) XCTAssertFalse(viewModel.context.viewState.showLoadingIndicator) diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMapView.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMapView.swift index 53064ae54..5a782d077 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMapView.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingMapView.swift @@ -68,7 +68,7 @@ class LocationSharingMapViewCoordinator: NSObject, MGLMapViewDelegate { private let avatarData: AvatarInputProtocol private let errorSubject: PassthroughSubject - @Binding var userLocation: CLLocationCoordinate2D? + @Binding private var userLocation: CLLocationCoordinate2D? init(avatarData: AvatarInputProtocol, errorSubject: PassthroughSubject, @@ -89,6 +89,10 @@ class LocationSharingMapViewCoordinator: NSObject, MGLMapViewDelegate { } func mapView(_ mapView: MGLMapView, didFailToLocateUserWithError error: Error) { + guard mapView.showsUserLocation else { + return + } + errorSubject.send(.failedLocatingUser) } @@ -97,11 +101,15 @@ class LocationSharingMapViewCoordinator: NSObject, MGLMapViewDelegate { } func mapView(_ mapView: MGLMapView, didChangeLocationManagerAuthorization manager: MGLLocationManager) { + guard mapView.showsUserLocation else { + return + } + switch manager.authorizationStatus { case .restricted: fallthrough case .denied: - errorSubject.send(.failedLocatingUser) + errorSubject.send(.invalidLocationAuthorization) default: break } diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingView.swift b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingView.swift index 9bc932536..be7ffe441 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingView.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/View/LocationSharingView.swift @@ -54,6 +54,7 @@ struct LocationSharingView: View { context.send(viewAction: .share) } label: { Image(uiImage: Asset.Images.locationShareIcon.image) + .accessibilityIdentifier("LocationSharingView.shareButton") } .disabled(!context.viewState.shareButtonEnabled) } else { @@ -69,6 +70,7 @@ struct LocationSharingView: View { .alert(item: $context.alertInfo) { info in if let secondaryButton = info.secondaryButton { return Alert(title: Text(info.title), + message: subtitleTextForAlertInfo(info), primaryButton: .default(Text(info.primaryButton.title)) { info.primaryButton.action?() }, @@ -77,6 +79,7 @@ struct LocationSharingView: View { }) } else { return Alert(title: Text(info.title), + message: subtitleTextForAlertInfo(info), dismissButton: .default(Text(info.primaryButton.title)) { info.primaryButton.action?() }) @@ -93,6 +96,14 @@ struct LocationSharingView: View { ActivityIndicator() } } + + private func subtitleTextForAlertInfo(_ alertInfo: LocationSharingErrorAlertInfo) -> Text? { + guard let subtitle = alertInfo.subtitle else { + return nil + } + + return Text(subtitle) + } } // MARK: - Previews diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift b/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift index 7476ac01a..2878648c8 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift @@ -31,12 +31,7 @@ final class PollEditFormCoordinator: Coordinator, Presentable { private let parameters: PollEditFormCoordinatorParameters private let pollEditFormHostingController: UIViewController - private var _pollEditFormViewModel: Any? = nil - - @available(iOS 14.0, *) - fileprivate var pollEditFormViewModel: PollEditFormViewModel { - return _pollEditFormViewModel as! PollEditFormViewModel - } + private var pollEditFormViewModel: PollEditFormViewModelProtocol // MARK: Public @@ -64,7 +59,7 @@ final class PollEditFormCoordinator: Coordinator, Presentable { let view = PollEditForm(viewModel: viewModel.context) - _pollEditFormViewModel = viewModel + pollEditFormViewModel = viewModel pollEditFormHostingController = VectorHostingController(rootView: view) } @@ -84,18 +79,18 @@ final class PollEditFormCoordinator: Coordinator, Presentable { let pollStartContent = self.buildPollContentWithDetails(details) - self.pollEditFormViewModel.dispatch(action: .startLoading) + self.pollEditFormViewModel.startLoading() 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)) + self.pollEditFormViewModel.stopLoading() self.completion?() } failure: { [weak self] error in guard let self = self else { return } MXLog.error("Failed creating poll with error: \(String(describing: error))") - self.pollEditFormViewModel.dispatch(action: .stopLoading(.failedCreatingPoll)) + self.pollEditFormViewModel.stopLoading(errorAlertType: .failedCreatingPoll) } case .update(let details): @@ -103,10 +98,10 @@ final class PollEditFormCoordinator: Coordinator, Presentable { fatalError() } - self.pollEditFormViewModel.dispatch(action: .startLoading) + self.pollEditFormViewModel.startLoading() guard let oldPollContent = MXEventContentPollStart(fromJSON: pollStartEvent.content) else { - self.pollEditFormViewModel.dispatch(action: .stopLoading(.failedUpdatingPoll)) + self.pollEditFormViewModel.stopLoading(errorAlertType: .failedUpdatingPoll) return } @@ -117,13 +112,13 @@ final class PollEditFormCoordinator: Coordinator, Presentable { newContent: newPollContent, localEcho: nil) { [weak self] result in guard let self = self else { return } - self.pollEditFormViewModel.dispatch(action: .stopLoading(nil)) + self.pollEditFormViewModel.stopLoading() self.completion?() } failure: { [weak self] error in guard let self = self else { return } MXLog.error("Failed updating poll with error: \(String(describing: error))") - self.pollEditFormViewModel.dispatch(action: .stopLoading(.failedUpdatingPoll)) + self.pollEditFormViewModel.stopLoading(errorAlertType: .failedUpdatingPoll) } } } diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormModels.swift b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormModels.swift index 346ba3d57..75fbf84ab 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormModels.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormModels.swift @@ -38,12 +38,6 @@ enum PollEditFormMode { case editing } -enum PollEditFormStateAction { - case viewAction(PollEditFormViewAction) - case startLoading - case stopLoading(PollEditFormErrorAlertInfo.AlertType?) -} - enum PollEditFormViewAction { case addAnswerOption case deleteAnswerOption(PollEditFormAnswerOption) diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormViewModel.swift b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormViewModel.swift index 022b4b727..ec6ca5e09 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormViewModel.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormViewModel.swift @@ -23,11 +23,11 @@ struct PollEditFormViewModelParameters { } @available(iOS 14, *) -typealias PollEditFormViewModelType = StateStoreViewModel< PollEditFormViewState, - PollEditFormStateAction, - PollEditFormViewAction > +typealias PollEditFormViewModelType = StateStoreViewModel @available(iOS 14, *) -class PollEditFormViewModel: PollEditFormViewModelType { +class PollEditFormViewModel: PollEditFormViewModelType, PollEditFormViewModelProtocol { private struct Constants { static let minAnswerOptionsCount = 2 @@ -71,40 +71,32 @@ class PollEditFormViewModel: PollEditFormViewModelType { completion?(.create(buildPollDetails())) case .update: completion?(.update(buildPollDetails())) - default: - dispatch(action: .viewAction(viewAction)) + case .addAnswerOption: + state.bindings.answerOptions.append(PollEditFormAnswerOption(text: "", maxLength: Constants.maxAnswerOptionLength)) + case .deleteAnswerOption(let answerOption): + state.bindings.answerOptions.removeAll { $0 == answerOption } } } - - override class func reducer(state: inout PollEditFormViewState, action: PollEditFormStateAction) { - switch action { - case .viewAction(let viewAction): - switch viewAction { - case .deleteAnswerOption(let answerOption): - state.bindings.answerOptions.removeAll { $0 == answerOption } - case .addAnswerOption: - state.bindings.answerOptions.append(PollEditFormAnswerOption(text: "", maxLength: Constants.maxAnswerOptionLength)) - default: - break - } - case .startLoading: - state.showLoadingIndicator = true - break - case .stopLoading(let error): - state.showLoadingIndicator = false - - switch error { - case .failedCreatingPoll: - state.bindings.alertInfo = PollEditFormErrorAlertInfo(id: .failedCreatingPoll, - title: VectorL10n.pollEditFormPostFailureTitle, - subtitle: VectorL10n.pollEditFormPostFailureSubtitle) - case .failedUpdatingPoll: - state.bindings.alertInfo = PollEditFormErrorAlertInfo(id: .failedUpdatingPoll, - title: VectorL10n.pollEditFormUpdateFailureTitle, - subtitle: VectorL10n.pollEditFormUpdateFailureSubtitle) - case .none: - break - } + + // MARK: - PollEditFormViewModelProtocol + + func startLoading() { + state.showLoadingIndicator = true + } + + func stopLoading(errorAlertType: PollEditFormErrorAlertInfo.AlertType?) { + state.showLoadingIndicator = false + + switch errorAlertType { + case .failedCreatingPoll: + state.bindings.alertInfo = PollEditFormErrorAlertInfo(id: .failedCreatingPoll, + title: VectorL10n.pollEditFormPostFailureTitle, + subtitle: VectorL10n.pollEditFormPostFailureSubtitle) + case .failedUpdatingPoll: + state.bindings.alertInfo = PollEditFormErrorAlertInfo(id: .failedUpdatingPoll, + title: VectorL10n.pollEditFormUpdateFailureTitle, + subtitle: VectorL10n.pollEditFormUpdateFailureSubtitle) + case .none: break } } @@ -115,8 +107,8 @@ class PollEditFormViewModel: PollEditFormViewModelType { return EditFormPollDetails(type: state.bindings.type, question: state.bindings.question.text.trimmingCharacters(in: .whitespacesAndNewlines), answerOptions: state.bindings.answerOptions.compactMap({ answerOption in - let text = answerOption.text.trimmingCharacters(in: .whitespacesAndNewlines) - return text.isEmpty ? nil : text - })) + let text = answerOption.text.trimmingCharacters(in: .whitespacesAndNewlines) + return text.isEmpty ? nil : text + })) } } diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormViewModelProtocol.swift b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormViewModelProtocol.swift new file mode 100644 index 000000000..da2824270 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormViewModelProtocol.swift @@ -0,0 +1,30 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +protocol PollEditFormViewModelProtocol { + var completion: ((PollEditFormViewModelResult) -> Void)? { get set } + + func startLoading() + func stopLoading(errorAlertType: PollEditFormErrorAlertInfo.AlertType?) +} + +extension PollEditFormViewModelProtocol { + func stopLoading() { + stopLoading(errorAlertType: nil) + } +} diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift index 13d1f4233..9502a6205 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift @@ -35,7 +35,7 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel private let selectedAnswerIdentifiersSubject = PassthroughSubject<[String], Never>() private var pollAggregator: PollAggregator - private var viewModel: TimelinePollViewModel! + private var viewModel: TimelinePollViewModelProtocol! private var cancellables = Set() // MARK: Public @@ -53,7 +53,7 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel pollAggregator.delegate = self viewModel = TimelinePollViewModel(timelinePollDetails: buildTimelinePollFrom(pollAggregator.poll)) - viewModel.callback = { [weak self] result in + viewModel.completion = { [weak self] result in guard let self = self else { return } switch result { @@ -76,7 +76,7 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel MXLog.error("[TimelinePollCoordinator]] Failed submitting response with error \(String(describing: error))") - self.viewModel.dispatch(action: .showAnsweringFailure) + self.viewModel.showAnsweringFailure() } } .store(in: &cancellables) @@ -102,14 +102,14 @@ final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDel func endPoll() { parameters.room.sendPollEnd(for: parameters.pollStartEvent, threadId: nil, localEcho: nil, success: nil) { [weak self] error in - self?.viewModel.dispatch(action: .showClosingFailure) + self?.viewModel.showClosingFailure() } } // MARK: - PollAggregatorDelegate func pollAggregatorDidUpdateData(_ aggregator: PollAggregator) { - viewModel.dispatch(action: .updateWithPoll(buildTimelinePollFrom(aggregator.poll))) + viewModel.updateWithPollDetails(buildTimelinePollFrom(aggregator.poll)) } func pollAggregatorDidStartLoading(_ aggregator: PollAggregator) { diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/Test/Unit/TimelinePollViewModelTests.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Test/Unit/TimelinePollViewModelTests.swift index 3de360418..3f3216356 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/Test/Unit/TimelinePollViewModelTests.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Test/Unit/TimelinePollViewModelTests.swift @@ -85,7 +85,7 @@ class TimelinePollViewModelTests: XCTestCase { } func testClosedSelection() { - context.viewState.poll.closed = true + viewModel.state.poll.closed = true context.send(viewAction: .selectAnswerOptionWithIdentifier("1")) context.send(viewAction: .selectAnswerOptionWithIdentifier("3")) @@ -96,7 +96,7 @@ class TimelinePollViewModelTests: XCTestCase { } func testSingleSelectionOnMax2Allowed() { - context.viewState.poll.maxAllowedSelections = 2 + viewModel.state.poll.maxAllowedSelections = 2 context.send(viewAction: .selectAnswerOptionWithIdentifier("1")) @@ -106,7 +106,7 @@ class TimelinePollViewModelTests: XCTestCase { } func testSingleReselectionOnMax2Allowed() { - context.viewState.poll.maxAllowedSelections = 2 + viewModel.state.poll.maxAllowedSelections = 2 context.send(viewAction: .selectAnswerOptionWithIdentifier("1")) context.send(viewAction: .selectAnswerOptionWithIdentifier("1")) @@ -117,7 +117,7 @@ class TimelinePollViewModelTests: XCTestCase { } func testMultipleSelectionOnMax2Allowed() { - context.viewState.poll.maxAllowedSelections = 2 + viewModel.state.poll.maxAllowedSelections = 2 context.send(viewAction: .selectAnswerOptionWithIdentifier("1")) context.send(viewAction: .selectAnswerOptionWithIdentifier("3")) diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift index da8abd7eb..e6a22e5e8 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift @@ -19,13 +19,6 @@ import SwiftUI typealias TimelinePollViewModelCallback = ((TimelinePollViewModelResult) -> Void) -enum TimelinePollStateAction { - case viewAction(TimelinePollViewAction, TimelinePollViewModelCallback?) - case updateWithPoll(TimelinePollDetails) - case showAnsweringFailure - case showClosingFailure -} - enum TimelinePollViewAction { case selectAnswerOptionWithIdentifier(String) } @@ -39,7 +32,7 @@ enum TimelinePollType { case undisclosed } -class TimelinePollAnswerOption: Identifiable { +struct TimelinePollAnswerOption: Identifiable { var id: String var text: String var count: UInt @@ -55,7 +48,15 @@ class TimelinePollAnswerOption: Identifiable { } } -class TimelinePollDetails { +extension MutableCollection where Element == TimelinePollAnswerOption { + mutating func updateEach(_ update: (inout Element) -> Void) { + for index in indices { + update(&self[index]) + } + } +} + +struct TimelinePollDetails { var question: String var answerOptions: [TimelinePollAnswerOption] var closed: Bool diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModel.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModel.swift index f28c7185c..e0574c0d7 100644 --- a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModel.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModel.swift @@ -19,10 +19,10 @@ import Combine @available(iOS 14, *) typealias TimelinePollViewModelType = StateStoreViewModel @available(iOS 14, *) -class TimelinePollViewModel: TimelinePollViewModelType { +class TimelinePollViewModel: TimelinePollViewModelType, TimelinePollViewModelProtocol { // MARK: - Properties @@ -30,7 +30,7 @@ class TimelinePollViewModel: TimelinePollViewModelType { // MARK: Public - var callback: TimelinePollViewModelCallback? + var completion: TimelinePollViewModelCallback? // MARK: - Setup @@ -42,49 +42,47 @@ class TimelinePollViewModel: TimelinePollViewModelType { override func process(viewAction: TimelinePollViewAction) { switch viewAction { - case .selectAnswerOptionWithIdentifier(_): - dispatch(action: .viewAction(viewAction, callback)) - } - } - - override class func reducer(state: inout TimelinePollViewState, action: TimelinePollStateAction) { - switch action { - case .viewAction(let viewAction, let callback): - switch viewAction { - - // Update local state. An update will be pushed from the coordinator once sent. - case .selectAnswerOptionWithIdentifier(let identifier): - guard !state.poll.closed else { - return - } - - if (state.poll.maxAllowedSelections == 1) { - updateSingleSelectPollLocalState(&state, selectedAnswerIdentifier: identifier, callback: callback) - } else { - updateMultiSelectPollLocalState(&state, selectedAnswerIdentifier: identifier, callback: callback) - } + + // Update local state. An update will be pushed from the coordinator once sent. + case .selectAnswerOptionWithIdentifier(let identifier): + guard !state.poll.closed else { + return + } + + if (state.poll.maxAllowedSelections == 1) { + updateSingleSelectPollLocalState(selectedAnswerIdentifier: identifier, callback: completion) + } else { + updateMultiSelectPollLocalState(&state, selectedAnswerIdentifier: identifier, callback: completion) } - case .updateWithPoll(let poll): - state.poll = poll - case .showAnsweringFailure: - state.bindings.alertInfo = TimelinePollErrorAlertInfo(id: .failedSubmittingAnswer, - title: VectorL10n.pollTimelineVoteNotRegisteredTitle, - subtitle: VectorL10n.pollTimelineVoteNotRegisteredSubtitle) - case .showClosingFailure: - state.bindings.alertInfo = TimelinePollErrorAlertInfo(id: .failedClosingPoll, - title: VectorL10n.pollTimelineNotClosedTitle, - subtitle: VectorL10n.pollTimelineNotClosedSubtitle) } } + // MARK: - TimelinePollViewModelProtocol + + func updateWithPollDetails(_ pollDetails: TimelinePollDetails) { + state.poll = pollDetails + } + + func showAnsweringFailure() { + state.bindings.alertInfo = TimelinePollErrorAlertInfo(id: .failedSubmittingAnswer, + title: VectorL10n.pollTimelineVoteNotRegisteredTitle, + subtitle: VectorL10n.pollTimelineVoteNotRegisteredSubtitle) + } + + func showClosingFailure() { + state.bindings.alertInfo = TimelinePollErrorAlertInfo(id: .failedClosingPoll, + title: VectorL10n.pollTimelineNotClosedTitle, + subtitle: VectorL10n.pollTimelineNotClosedSubtitle) + } + // MARK: - Private - static func updateSingleSelectPollLocalState(_ state: inout TimelinePollViewState, selectedAnswerIdentifier: String, callback: TimelinePollViewModelCallback?) { - for answerOption in state.poll.answerOptions { + func updateSingleSelectPollLocalState(selectedAnswerIdentifier: String, callback: TimelinePollViewModelCallback?) { + state.poll.answerOptions.updateEach { answerOption in if answerOption.selected { answerOption.selected = false - if(answerOption.count > 0) { + if(state.poll.answerOptions.count > 0) { answerOption.count = answerOption.count - 1 state.poll.totalAnswerCount -= 1 } @@ -100,7 +98,7 @@ class TimelinePollViewModel: TimelinePollViewModelType { informCoordinatorOfSelectionUpdate(state: state, callback: callback) } - static func updateMultiSelectPollLocalState(_ state: inout TimelinePollViewState, selectedAnswerIdentifier: String, callback: TimelinePollViewModelCallback?) { + func updateMultiSelectPollLocalState(_ state: inout TimelinePollViewState, selectedAnswerIdentifier: String, callback: TimelinePollViewModelCallback?) { let selectedAnswerOptions = state.poll.answerOptions.filter { $0.selected == true } let isDeselecting = selectedAnswerOptions.filter { $0.id == selectedAnswerIdentifier }.count > 0 @@ -109,7 +107,11 @@ class TimelinePollViewModel: TimelinePollViewModelType { return } - for answerOption in state.poll.answerOptions where answerOption.id == selectedAnswerIdentifier { + state.poll.answerOptions.updateEach { answerOption in + if (answerOption.id != selectedAnswerIdentifier) { + return + } + if answerOption.selected { answerOption.selected = false answerOption.count -= 1 @@ -124,7 +126,7 @@ class TimelinePollViewModel: TimelinePollViewModelType { informCoordinatorOfSelectionUpdate(state: state, callback: callback) } - static func informCoordinatorOfSelectionUpdate(state: TimelinePollViewState, callback: TimelinePollViewModelCallback?) { + func informCoordinatorOfSelectionUpdate(state: TimelinePollViewState, callback: TimelinePollViewModelCallback?) { let selectedIdentifiers = state.poll.answerOptions.compactMap { answerOption in answerOption.selected ? answerOption.id : nil } diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModelProtocol.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModelProtocol.swift new file mode 100644 index 000000000..adaf6ad15 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModelProtocol.swift @@ -0,0 +1,27 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +protocol TimelinePollViewModelProtocol { + @available(iOS 14, *) + var context: TimelinePollViewModelType.Context { get } + var completion: ((TimelinePollViewModelResult) -> Void)? { get set } + + func updateWithPollDetails(_ pollDetails: TimelinePollDetails) + func showAnsweringFailure() + func showClosingFailure() +} diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift index 4163d0668..2c08b20a9 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift @@ -23,6 +23,11 @@ protocol UserSuggestionCoordinatorDelegate: AnyObject { func userSuggestionCoordinator(_ coordinator: UserSuggestionCoordinator, didRequestMentionForMember member: MXRoomMember, textTrigger: String?) } +struct UserSuggestionCoordinatorParameters { + let mediaManager: MXMediaManager + let room: MXRoom +} + @available(iOS 14.0, *) final class UserSuggestionCoordinator: Coordinator, Presentable { @@ -53,11 +58,12 @@ final class UserSuggestionCoordinator: Coordinator, Presentable { roomMemberProvider = UserSuggestionCoordinatorRoomMemberProvider(room: parameters.room) userSuggestionService = UserSuggestionService(roomMemberProvider: roomMemberProvider) - userSuggestionViewModel = UserSuggestionViewModel.makeUserSuggestionViewModel(userSuggestionService: userSuggestionService) - let view = UserSuggestionList(viewModel: userSuggestionViewModel.context) + let viewModel = UserSuggestionViewModel(userSuggestionService: userSuggestionService) + let view = UserSuggestionList(viewModel: viewModel.context) .addDependency(AvatarService.instantiate(mediaManager: parameters.mediaManager)) + userSuggestionViewModel = viewModel userSuggestionHostingController = VectorHostingController(rootView: view) userSuggestionViewModel.completion = { [weak self] result in @@ -90,7 +96,6 @@ final class UserSuggestionCoordinator: Coordinator, Presentable { } } -@available(iOS 14.0, *) private class UserSuggestionCoordinatorRoomMemberProvider: RoomMembersProviderProtocol { private let room: MXRoom diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift index a2a59ec86..5ac56294f 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift @@ -17,19 +17,16 @@ import Foundation import Combine -@available(iOS 14.0, *) struct RoomMembersProviderMember { var userId: String var displayName: String var avatarUrl: String } -@available(iOS 14.0, *) protocol RoomMembersProviderProtocol { func fetchMembers(_ members: @escaping ([RoomMembersProviderMember]) -> Void) } -@available(iOS 14.0, *) struct UserSuggestionServiceItem: UserSuggestionItemProtocol { let userId: String let displayName: String? diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift index 44009d0c8..d7be97eb4 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift @@ -17,7 +17,6 @@ import Foundation import Combine -@available(iOS 14.0, *) protocol UserSuggestionItemProtocol: Avatarable { var userId: String { get } var displayName: String? { get } @@ -36,7 +35,6 @@ protocol UserSuggestionServiceProtocol { // MARK: Avatarable -@available(iOS 14.0, *) extension UserSuggestionItemProtocol { var mxContentUri: String? { avatarUrl diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewState.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionModels.swift similarity index 83% rename from RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewState.swift rename to RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionModels.swift index 0b992b6d1..8bc107a3c 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewState.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionModels.swift @@ -15,16 +15,21 @@ // import Foundation -import Combine -@available(iOS 14.0, *) +enum UserSuggestionViewAction { + case selectedItem(UserSuggestionViewStateItem) +} + +enum UserSuggestionViewModelResult { + case selectedItemWithIdentifier(String) +} + struct UserSuggestionViewStateItem: Identifiable { let id: String let avatar: AvatarInputProtocol? let displayName: String? } -@available(iOS 14.0, *) struct UserSuggestionViewState: BindableState { var items: [UserSuggestionViewStateItem] } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/Mock/MockUserSuggestionScreenState.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionScreenState.swift similarity index 94% rename from RiotSwiftUI/Modules/Room/UserSuggestion/Service/Mock/MockUserSuggestionScreenState.swift rename to RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionScreenState.swift index 16941ab76..9c290f6ae 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/Mock/MockUserSuggestionScreenState.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionScreenState.swift @@ -29,7 +29,7 @@ enum MockUserSuggestionScreenState: MockScreenState, CaseIterable { var screenView: ([Any], AnyView) { let service = UserSuggestionService(roomMemberProvider: self) - let listViewModel = UserSuggestionViewModel.makeUserSuggestionViewModel(userSuggestionService: service) + let listViewModel = UserSuggestionViewModel(userSuggestionService: service) let viewModel = UserSuggestionListWithInputViewModel(listViewModel: listViewModel) { textMessage in service.processTextMessage(textMessage) diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/ViewModel/UserSuggestionViewModel.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModel.swift similarity index 56% rename from RiotSwiftUI/Modules/Room/UserSuggestion/ViewModel/UserSuggestionViewModel.swift rename to RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModel.swift index 62ba522e4..51a3aa7f7 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/ViewModel/UserSuggestionViewModel.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModel.swift @@ -17,11 +17,12 @@ import SwiftUI import Combine -@available(iOS 14, *) +@available(iOS 14.0, *) typealias UserSuggestionViewModelType = StateStoreViewModel -@available(iOS 14, *) + +@available(iOS 14.0, *) class UserSuggestionViewModel: UserSuggestionViewModelType, UserSuggestionViewModelProtocol { // MARK: - Properties @@ -35,30 +36,21 @@ class UserSuggestionViewModel: UserSuggestionViewModelType, UserSuggestionViewMo var completion: ((UserSuggestionViewModelResult) -> Void)? // MARK: - Setup - - static func makeUserSuggestionViewModel(userSuggestionService: UserSuggestionServiceProtocol) -> UserSuggestionViewModelProtocol { - return UserSuggestionViewModel(userSuggestionService: userSuggestionService) - } - private init(userSuggestionService: UserSuggestionServiceProtocol) { + init(userSuggestionService: UserSuggestionServiceProtocol) { self.userSuggestionService = userSuggestionService - super.init(initialViewState: Self.defaultState(userSuggestionService: userSuggestionService)) - setupItemsObserving() - } - - private func setupItemsObserving() { - let updatePublisher = userSuggestionService.items - .map(UserSuggestionStateAction.updateWithItems) - .eraseToAnyPublisher() - dispatch(actionPublisher: updatePublisher) - } - - private static func defaultState(userSuggestionService: UserSuggestionServiceProtocol) -> UserSuggestionViewState { - let viewStateItems = userSuggestionService.items.value.map { suggestionItem in + + let items = userSuggestionService.items.value.map { suggestionItem in return UserSuggestionViewStateItem(id: suggestionItem.userId, avatar: suggestionItem, displayName: suggestionItem.displayName) } - return UserSuggestionViewState(items: viewStateItems) + super.init(initialViewState: UserSuggestionViewState(items: items)) + + userSuggestionService.items.sink { items in + self.state.items = items.map({ item in + UserSuggestionViewStateItem(id: item.userId, avatar: item, displayName: item.displayName) + }) + }.store(in: &cancellables) } // MARK: - Public @@ -69,13 +61,4 @@ class UserSuggestionViewModel: UserSuggestionViewModelType, UserSuggestionViewMo completion?(.selectedItemWithIdentifier(item.id)) } } - - override class func reducer(state: inout UserSuggestionViewState, action: UserSuggestionStateAction) { - switch action { - case .updateWithItems(let items): - state.items = items.map({ item in - UserSuggestionViewStateItem(id: item.userId, avatar: item, displayName: item.displayName) - }) - } - } } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/ViewModel/UserSuggestionViewModelProtocol.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModelProtocol.swift similarity index 76% rename from RiotSwiftUI/Modules/Room/UserSuggestion/ViewModel/UserSuggestionViewModelProtocol.swift rename to RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModelProtocol.swift index 169b89735..7a04cf8c4 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/ViewModel/UserSuggestionViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/UserSuggestionViewModelProtocol.swift @@ -16,12 +16,6 @@ import Foundation -@available(iOS 14, *) protocol UserSuggestionViewModelProtocol { - - static func makeUserSuggestionViewModel(userSuggestionService: UserSuggestionServiceProtocol) -> UserSuggestionViewModelProtocol - - var context: UserSuggestionViewModelType.Context { get } - var completion: ((UserSuggestionViewModelResult) -> Void)? { get set } } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListWithInput.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListWithInput.swift index 40573b75c..6f26c19ee 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListWithInput.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListWithInput.swift @@ -18,7 +18,7 @@ import SwiftUI @available(iOS 14.0, *) struct UserSuggestionListWithInputViewModel { - let listViewModel: UserSuggestionViewModelProtocol + let listViewModel: UserSuggestionViewModel let callback: (String)->() } diff --git a/RiotSwiftUI/Modules/Template/SimpleScreenExample/TemplateSimpleScreenModels.swift b/RiotSwiftUI/Modules/Template/SimpleScreenExample/TemplateSimpleScreenModels.swift index 5de592c73..2238fc709 100644 --- a/RiotSwiftUI/Modules/Template/SimpleScreenExample/TemplateSimpleScreenModels.swift +++ b/RiotSwiftUI/Modules/Template/SimpleScreenExample/TemplateSimpleScreenModels.swift @@ -47,10 +47,6 @@ extension TemplateSimpleScreenPromptType: Identifiable, CaseIterable { // MARK: View model -enum TemplateSimpleScreenStateAction { - case viewAction(TemplateSimpleScreenViewAction) -} - enum TemplateSimpleScreenViewModelResult { case accept case cancel diff --git a/RiotSwiftUI/Modules/Template/SimpleScreenExample/TemplateSimpleScreenViewModel.swift b/RiotSwiftUI/Modules/Template/SimpleScreenExample/TemplateSimpleScreenViewModel.swift index dd1664969..b67286866 100644 --- a/RiotSwiftUI/Modules/Template/SimpleScreenExample/TemplateSimpleScreenViewModel.swift +++ b/RiotSwiftUI/Modules/Template/SimpleScreenExample/TemplateSimpleScreenViewModel.swift @@ -18,7 +18,7 @@ import SwiftUI @available(iOS 14, *) typealias TemplateSimpleScreenViewModelType = StateStoreViewModel @available(iOS 14, *) class TemplateSimpleScreenViewModel: TemplateSimpleScreenViewModelType, TemplateSimpleScreenViewModelProtocol { @@ -45,23 +45,10 @@ class TemplateSimpleScreenViewModel: TemplateSimpleScreenViewModelType, Template completion?(.accept) case .cancel: completion?(.cancel) - case .incrementCount, .decrementCount: - dispatch(action: .viewAction(viewAction)) + case .incrementCount: + state.count += 1 + case .decrementCount: + state.count -= 1 } } - - override class func reducer(state: inout TemplateSimpleScreenViewState, action: TemplateSimpleScreenStateAction) { - switch action { - case .viewAction(let viewAction): - switch viewAction { - case .incrementCount: - state.count += 1 - case .decrementCount: - state.count -= 1 - case .accept, .cancel: - break - } - } - UILog.debug("[TemplateSimpleScreenViewModel] reducer with action \(action) produced state: \(state)") - } } diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/TemplateUserProfileModels.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/TemplateUserProfileModels.swift index 62e4c895c..e57ce601e 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/TemplateUserProfileModels.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/TemplateUserProfileModels.swift @@ -41,11 +41,6 @@ extension TemplateUserProfilePresence: Identifiable, CaseIterable { // MARK: View model -enum TemplateUserProfileStateAction { - case viewAction(TemplateUserProfileViewAction) - case updatePresence(TemplateUserProfilePresence) -} - enum TemplateUserProfileViewModelResult { case cancel case done diff --git a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/TemplateUserProfileViewModel.swift b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/TemplateUserProfileViewModel.swift index e476b467e..a6dfd50c1 100644 --- a/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/TemplateUserProfileViewModel.swift +++ b/RiotSwiftUI/Modules/Template/SimpleUserProfileExample/TemplateUserProfileViewModel.swift @@ -19,7 +19,7 @@ import Combine @available(iOS 14, *) typealias TemplateUserProfileViewModelType = StateStoreViewModel @available(iOS 14, *) class TemplateUserProfileViewModel: TemplateUserProfileViewModelType, TemplateUserProfileViewModelProtocol { @@ -54,49 +54,28 @@ class TemplateUserProfileViewModel: TemplateUserProfileViewModelType, TemplateUs count: 0 ) } - + private func setupPresenceObserving() { - let presenceUpdatePublisher = templateUserProfileService.presenceSubject - .map(TemplateUserProfileStateAction.updatePresence) - .eraseToAnyPublisher() - dispatch(actionPublisher: presenceUpdatePublisher) + templateUserProfileService + .presenceSubject + .sink(receiveValue: { [weak self] presence in + self?.state.presence = presence + }) + .store(in: &cancellables) } - + // MARK: - Public override func process(viewAction: TemplateUserProfileViewAction) { switch viewAction { case .cancel: - cancel() + completion?(.cancel) case .done: - done() - case .incrementCount, .decrementCount: - dispatch(action: .viewAction(viewAction)) + completion?(.done) + case .incrementCount: + state.count += 1 + case .decrementCount: + state.count -= 1 } } - - override class func reducer(state: inout TemplateUserProfileViewState, action: TemplateUserProfileStateAction) { - switch action { - case .updatePresence(let presence): - state.presence = presence - case .viewAction(let viewAction): - switch viewAction { - case .incrementCount: - state.count += 1 - case .decrementCount: - state.count -= 1 - case .cancel, .done: - break - } - } - UILog.debug("[TemplateUserProfileViewModel] reducer with action \(action) produced state: \(state)") - } - - private func done() { - completion?(.done) - } - - private func cancel() { - completion?(.cancel) - } } diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/TemplateRoomChatModels.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/TemplateRoomChatModels.swift index de30950e2..e9aa09141 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/TemplateRoomChatModels.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/TemplateRoomChatModels.swift @@ -83,13 +83,6 @@ enum TemplateRoomChatRoomInitializationStatus { case failedToInitialize } -/// Actions to be performed on the `ViewModel` State -enum TemplateRoomChatStateAction { - case updateRoomInitializationStatus(TemplateRoomChatRoomInitializationStatus) - case updateBubbles([TemplateRoomChatBubble]) - case clearMessageInput -} - /// Actions sent by the `ViewModel` to the `Coordinator` enum TemplateRoomChatViewModelAction { case done diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/TemplateRoomChatViewModel.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/TemplateRoomChatViewModel.swift index 2be3c5818..0a6e2a0d2 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/TemplateRoomChatViewModel.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomChat/TemplateRoomChatViewModel.swift @@ -19,7 +19,7 @@ import Combine @available(iOS 14, *) typealias TemplateRoomChatViewModelType = StateStoreViewModel @available(iOS 14, *) @@ -48,21 +48,22 @@ class TemplateRoomChatViewModel: TemplateRoomChatViewModelType, TemplateRoomChat } private func setupRoomInitializationObserving() { - let initializationPublisher = templateRoomChatService + templateRoomChatService .roomInitializationStatus - .map(TemplateRoomChatStateAction.updateRoomInitializationStatus) - .eraseToAnyPublisher() - - dispatch(actionPublisher: initializationPublisher) + .sink { [weak self] status in + self?.state.roomInitializationStatus = status + } + .store(in: &cancellables) } - + private func setupMessageObserving() { - let messageActionPublisher = templateRoomChatService + templateRoomChatService .chatMessagesSubject .map(Self.makeBubbles(messages:)) - .map(TemplateRoomChatStateAction.updateBubbles) - .eraseToAnyPublisher() - dispatch(actionPublisher: messageActionPublisher) + .sink { [weak self] bubbles in + self?.state.bubbles = bubbles + } + .store(in: &cancellables) } private static func defaultState(templateRoomChatService: TemplateRoomChatServiceProtocol) -> TemplateRoomChatViewState { @@ -117,27 +118,10 @@ class TemplateRoomChatViewModel: TemplateRoomChatViewModelType, TemplateRoomChat override func process(viewAction: TemplateRoomChatViewAction) { switch viewAction { case .done: - done() + callback?(.done) case .sendMessage: templateRoomChatService.send(textMessage: state.bindings.messageInput) - dispatch(action: .clearMessageInput) - } - } - - override class func reducer(state: inout TemplateRoomChatViewState, action: TemplateRoomChatStateAction) { - switch action { - case .updateRoomInitializationStatus(let status): - state.roomInitializationStatus = status - case .clearMessageInput: state.bindings.messageInput = "" - case .updateBubbles(let bubbles): - state.bubbles = bubbles } } - - // MARK: - Private - - private func done() { - callback?(.done) - } } diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/TemplateRoomListModels.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/TemplateRoomListModels.swift index 6adb43c0c..57452cc1c 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/TemplateRoomListModels.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/TemplateRoomListModels.swift @@ -32,11 +32,6 @@ enum TemplateRoomListCoordinatorAction { // MARK: - View model -/// Actions to be performed on the `ViewModel` State -enum TemplateRoomListStateAction { - case updateRooms([TemplateRoomListRoom]) -} - /// Actions sent by the`ViewModel` to the `Coordinator`. enum TemplateRoomListViewModelAction { case didSelectRoom(String) diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/TemplateRoomListViewModel.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/TemplateRoomListViewModel.swift index 338146605..4c906c237 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/TemplateRoomListViewModel.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/TemplateRoomListViewModel.swift @@ -19,7 +19,7 @@ import Combine @available(iOS 14, *) typealias TemplateRoomListViewModelType = StateStoreViewModel @available(iOS 14.0, *) class TemplateRoomListViewModel: TemplateRoomListViewModelType, TemplateRoomListViewModelProtocol { @@ -47,10 +47,12 @@ class TemplateRoomListViewModel: TemplateRoomListViewModelType, TemplateRoomList } private func startObservingRooms() { - let roomsUpdatePublisher = templateRoomListService.roomsSubject - .map(TemplateRoomListStateAction.updateRooms) - .eraseToAnyPublisher() - dispatch(actionPublisher: roomsUpdatePublisher) + templateRoomListService + .roomsSubject + .sink { [weak self] rooms in + self?.state.rooms = rooms + } + .store(in: &cancellables) } // MARK: - Public @@ -64,13 +66,6 @@ class TemplateRoomListViewModel: TemplateRoomListViewModelType, TemplateRoomList } } - override class func reducer(state: inout TemplateRoomListViewState, action: TemplateRoomListStateAction) { - switch action { - case .updateRooms(let rooms): - state.rooms = rooms - } - } - // MARK: - Private private func done() { diff --git a/changelog.d/5159.feature.change b/changelog.d/5159.feature similarity index 100% rename from changelog.d/5159.feature.change rename to changelog.d/5159.feature diff --git a/changelog.d/5175.bugfix b/changelog.d/5175.bugfix new file mode 100644 index 000000000..3b65ea95a --- /dev/null +++ b/changelog.d/5175.bugfix @@ -0,0 +1 @@ +Accepting a Space Invite has shouty button labels \ No newline at end of file diff --git a/changelog.d/5298.feature b/changelog.d/5298.feature new file mode 100644 index 000000000..195f5df7e --- /dev/null +++ b/changelog.d/5298.feature @@ -0,0 +1 @@ +Remove location sharing settings entry and enable it by default. \ No newline at end of file diff --git a/changelog.d/5465.bugfix b/changelog.d/5465.bugfix new file mode 100644 index 000000000..170cffd83 --- /dev/null +++ b/changelog.d/5465.bugfix @@ -0,0 +1 @@ +Fixes media library freezing under iOS 15.2.