diff --git a/Config/BuildSettings.swift b/Config/BuildSettings.swift index fe1bdcf8e..98483d53f 100644 --- a/Config/BuildSettings.swift +++ b/Config/BuildSettings.swift @@ -375,6 +375,6 @@ final class BuildSettings: NSObject { return false } - return false + return true } } diff --git a/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_default.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_default.imageset/Contents.json new file mode 100644 index 000000000..945c5c337 --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_default.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "poll_type_checkbox_default.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "poll_type_checkbox_default@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "poll_type_checkbox_default@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_default.imageset/poll_type_checkbox_default.png b/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_default.imageset/poll_type_checkbox_default.png new file mode 100644 index 000000000..84e419079 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_default.imageset/poll_type_checkbox_default.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_default.imageset/poll_type_checkbox_default@2x.png b/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_default.imageset/poll_type_checkbox_default@2x.png new file mode 100644 index 000000000..7e6083bc3 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_default.imageset/poll_type_checkbox_default@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_default.imageset/poll_type_checkbox_default@3x.png b/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_default.imageset/poll_type_checkbox_default@3x.png new file mode 100644 index 000000000..316a8eab7 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_default.imageset/poll_type_checkbox_default@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_selected.imageset/Contents.json b/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_selected.imageset/Contents.json new file mode 100644 index 000000000..b7fbce06b --- /dev/null +++ b/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_selected.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "poll_type_checkbox_selected.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "poll_type_checkbox_selected@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "poll_type_checkbox_selected@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_selected.imageset/poll_type_checkbox_selected.png b/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_selected.imageset/poll_type_checkbox_selected.png new file mode 100644 index 000000000..6a744d6be Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_selected.imageset/poll_type_checkbox_selected.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_selected.imageset/poll_type_checkbox_selected@2x.png b/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_selected.imageset/poll_type_checkbox_selected@2x.png new file mode 100644 index 000000000..67c3bbd64 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_selected.imageset/poll_type_checkbox_selected@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_selected.imageset/poll_type_checkbox_selected@3x.png b/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_selected.imageset/poll_type_checkbox_selected@3x.png new file mode 100644 index 000000000..a4cd21452 Binary files /dev/null and b/Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_selected.imageset/poll_type_checkbox_selected@3x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Threads/threads_icon.imageset/Thread.png b/Riot/Assets/Images.xcassets/Room/Threads/threads_icon.imageset/Thread.png index b2cc0cb72..21add0227 100644 Binary files a/Riot/Assets/Images.xcassets/Room/Threads/threads_icon.imageset/Thread.png and b/Riot/Assets/Images.xcassets/Room/Threads/threads_icon.imageset/Thread.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Threads/threads_icon.imageset/Thread@2x.png b/Riot/Assets/Images.xcassets/Room/Threads/threads_icon.imageset/Thread@2x.png index fe86d55c3..e5c85a57f 100644 Binary files a/Riot/Assets/Images.xcassets/Room/Threads/threads_icon.imageset/Thread@2x.png and b/Riot/Assets/Images.xcassets/Room/Threads/threads_icon.imageset/Thread@2x.png differ diff --git a/Riot/Assets/Images.xcassets/Room/Threads/threads_icon.imageset/Thread@3x.png b/Riot/Assets/Images.xcassets/Room/Threads/threads_icon.imageset/Thread@3x.png index fae5443be..2204e1ccf 100644 Binary files a/Riot/Assets/Images.xcassets/Room/Threads/threads_icon.imageset/Thread@3x.png and b/Riot/Assets/Images.xcassets/Room/Threads/threads_icon.imageset/Thread@3x.png differ diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index 228729ca0..bb405216d 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -446,6 +446,7 @@ Tap the + to start adding people."; "threads_empty_info_my" = "Reply to an ongoing thread or tap a message and use “Thread” to start a new one."; "threads_empty_tip" = "Tip: Tap a message and use “Thread” to start one."; "threads_empty_show_all_threads" = "Show all threads"; +"message_from_a_thread" = "From a thread"; "media_type_accessibility_image" = "Image"; "media_type_accessibility_audio" = "Audio"; @@ -1840,6 +1841,8 @@ Tap the + to start adding people."; "poll_edit_form_create_poll" = "Create poll"; +"poll_edit_form_poll_type" = "Poll type"; + "poll_edit_form_poll_question_or_topic" = "Poll question or topic"; "poll_edit_form_question_or_topic" = "Question or topic"; @@ -1856,6 +1859,18 @@ Tap the + to start adding people."; "poll_edit_form_post_failure_subtitle" = "Please try again"; +"poll_edit_form_update_failure_title" = "Failed to update poll"; + +"poll_edit_form_update_failure_subtitle" = "Please try again"; + +"poll_edit_form_poll_type_open" = "Open poll"; + +"poll_edit_form_poll_type_open_description" = "Voters see results as soon as they have voted"; + +"poll_edit_form_poll_type_closed" = "Closed poll"; + +"poll_edit_form_poll_type_closed_description" = "Results are only revealed when you end the poll"; + "poll_timeline_one_vote" = "1 vote"; "poll_timeline_votes_count" = "%lu votes"; diff --git a/Riot/Categories/UIView+Toast.swift b/Riot/Categories/UIView+Toast.swift index e00858afc..b8d2db9aa 100644 --- a/Riot/Categories/UIView+Toast.swift +++ b/Riot/Categories/UIView+Toast.swift @@ -95,7 +95,7 @@ private class ToastOperation: AsyncOperation { private enum Constants { static let margin: UIEdgeInsets = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 16) - static let animationDuration: TimeInterval = 0.25 + static let animationDuration: TimeInterval = 0.15 static let timeBetweenToasts: TimeInterval = 0.5 } diff --git a/Riot/Generated/Images.swift b/Riot/Generated/Images.swift index 69fedcb62..d8c98b114 100644 --- a/Riot/Generated/Images.swift +++ b/Riot/Generated/Images.swift @@ -156,6 +156,8 @@ internal enum Asset { internal static let pollDeleteOptionIcon = ImageAsset(name: "poll_delete_option_icon") internal static let pollEditIcon = ImageAsset(name: "poll_edit_icon") internal static let pollEndIcon = ImageAsset(name: "poll_end_icon") + internal static let pollTypeCheckboxDefault = ImageAsset(name: "poll_type_checkbox_default") + internal static let pollTypeCheckboxSelected = ImageAsset(name: "poll_type_checkbox_selected") internal static let pollWinnerIcon = ImageAsset(name: "poll_winner_icon") internal static let threadsFilter = ImageAsset(name: "threads_filter") internal static let threadsFilterApplied = ImageAsset(name: "threads_filter_applied") diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index 49ebc94ff..992396767 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -2319,6 +2319,10 @@ public class VectorL10n: NSObject { public static var mediaTypeAccessibilityVideo: String { return VectorL10n.tr("Vector", "media_type_accessibility_video") } + /// From a thread + public static var messageFromAThread: String { + return VectorL10n.tr("Vector", "message_from_a_thread") + } /// More public static var more: String { return VectorL10n.tr("Vector", "more") @@ -2539,6 +2543,26 @@ public class VectorL10n: NSObject { public static var pollEditFormPollQuestionOrTopic: String { return VectorL10n.tr("Vector", "poll_edit_form_poll_question_or_topic") } + /// Poll type + public static var pollEditFormPollType: String { + return VectorL10n.tr("Vector", "poll_edit_form_poll_type") + } + /// Closed poll + public static var pollEditFormPollTypeClosed: String { + return VectorL10n.tr("Vector", "poll_edit_form_poll_type_closed") + } + /// Results are only revealed when you end the poll + public static var pollEditFormPollTypeClosedDescription: String { + return VectorL10n.tr("Vector", "poll_edit_form_poll_type_closed_description") + } + /// Open poll + public static var pollEditFormPollTypeOpen: String { + return VectorL10n.tr("Vector", "poll_edit_form_poll_type_open") + } + /// Voters see results as soon as they have voted + public static var pollEditFormPollTypeOpenDescription: String { + return VectorL10n.tr("Vector", "poll_edit_form_poll_type_open_description") + } /// Please try again public static var pollEditFormPostFailureSubtitle: String { return VectorL10n.tr("Vector", "poll_edit_form_post_failure_subtitle") @@ -2552,6 +2576,14 @@ public class VectorL10n: NSObject { return VectorL10n.tr("Vector", "poll_edit_form_question_or_topic") } /// Please try again + public static var pollEditFormUpdateFailureSubtitle: String { + return VectorL10n.tr("Vector", "poll_edit_form_update_failure_subtitle") + } + /// Failed to update poll + public static var pollEditFormUpdateFailureTitle: String { + return VectorL10n.tr("Vector", "poll_edit_form_update_failure_title") + } + /// Please try again public static var pollTimelineNotClosedSubtitle: String { return VectorL10n.tr("Vector", "poll_timeline_not_closed_subtitle") } diff --git a/Riot/Managers/Settings/RiotSettings.swift b/Riot/Managers/Settings/RiotSettings.swift index 2550430d6..9b18bec6d 100644 --- a/Riot/Managers/Settings/RiotSettings.swift +++ b/Riot/Managers/Settings/RiotSettings.swift @@ -188,10 +188,7 @@ final class RiotSettings: NSObject { @UserDefault(key: "roomScreenAllowFilesAction", defaultValue: BuildSettings.roomScreenAllowFilesAction, storage: defaults) var roomScreenAllowFilesAction - - @UserDefault(key: "roomScreenAllowPollsAction", defaultValue: false, storage: defaults) - var roomScreenAllowPollsAction - + @UserDefault(key: "roomScreenAllowLocationAction", defaultValue: false, storage: defaults) var roomScreenAllowLocationAction diff --git a/Riot/Modules/GlobalSearch/Messages/DataSources/HomeMessagesSearchDataSource.m b/Riot/Modules/GlobalSearch/Messages/DataSources/HomeMessagesSearchDataSource.m index 950715c17..fe69086ae 100644 --- a/Riot/Modules/GlobalSearch/Messages/DataSources/HomeMessagesSearchDataSource.m +++ b/Riot/Modules/GlobalSearch/Messages/DataSources/HomeMessagesSearchDataSource.m @@ -66,7 +66,10 @@ if (cellData) { // Highlight the search pattern - [cellData highlightPatternInTextMessage:self.searchText withForegroundColor:ThemeService.shared.theme.tintColor andFont:patternFont]; + [cellData highlightPatternInTextMessage:self.searchText + withBackgroundColor:[UIColor clearColor] + foregroundColor:ThemeService.shared.theme.tintColor + andFont:patternFont]; // Use profile information as data to display MXSearchUserProfile *userProfile = result.context.profileInfo[result.result.sender]; @@ -115,6 +118,60 @@ // Display date for each message [bubbleCell addDateLabel]; + + RoomBubbleCellData *cellData = (RoomBubbleCellData*)[self cellDataAtIndex:indexPath.row]; + MXEvent *event = cellData.events.firstObject; + + if (event) + { + if (cellData.hasThreadRoot) + { + MXThread *thread = cellData.bubbleComponents.firstObject.thread; + ThreadSummaryView *threadSummaryView = [[ThreadSummaryView alloc] initWithThread:thread]; + [bubbleCell.tmpSubviews addObject:threadSummaryView]; + + threadSummaryView.translatesAutoresizingMaskIntoConstraints = NO; + [bubbleCell.contentView addSubview:threadSummaryView]; + + CGFloat leftMargin = RoomBubbleCellLayout.reactionsViewLeftMargin; + + CGRect bubbleComponentFrame = [bubbleCell componentFrameInContentViewForIndex:0]; + CGFloat bottomPositionY = bubbleComponentFrame.origin.y + bubbleComponentFrame.size.height; + + // Set constraints for the summary view + [NSLayoutConstraint activateConstraints: @[ + [threadSummaryView.leadingAnchor constraintEqualToAnchor:threadSummaryView.superview.leadingAnchor + constant:leftMargin], + [threadSummaryView.topAnchor constraintEqualToAnchor:threadSummaryView.superview.topAnchor + constant:bottomPositionY + RoomBubbleCellLayout.threadSummaryViewTopMargin], + [threadSummaryView.heightAnchor constraintEqualToConstant:[ThreadSummaryView contentViewHeightForThread:thread fitting:cellData.maxTextViewWidth]], + [threadSummaryView.trailingAnchor constraintLessThanOrEqualToAnchor:threadSummaryView.superview.trailingAnchor constant:-RoomBubbleCellLayout.reactionsViewRightMargin] + ]]; + } + else if (event.isInThread) + { + FromThreadView *fromThreadView = [FromThreadView instantiate]; + [bubbleCell.tmpSubviews addObject:fromThreadView]; + + fromThreadView.translatesAutoresizingMaskIntoConstraints = NO; + [bubbleCell.contentView addSubview:fromThreadView]; + + CGFloat leftMargin = RoomBubbleCellLayout.reactionsViewLeftMargin; + + CGRect bubbleComponentFrame = [bubbleCell componentFrameInContentViewForIndex:0]; + CGFloat bottomPositionY = bubbleComponentFrame.origin.y + bubbleComponentFrame.size.height; + + // Set constraints for the summary view + [NSLayoutConstraint activateConstraints: @[ + [fromThreadView.leadingAnchor constraintEqualToAnchor:fromThreadView.superview.leadingAnchor + constant:leftMargin], + [fromThreadView.topAnchor constraintEqualToAnchor:fromThreadView.superview.topAnchor + constant:bottomPositionY + RoomBubbleCellLayout.fromThreadViewTopMargin], + [fromThreadView.heightAnchor constraintEqualToConstant:[FromThreadView contentViewHeightForEvent:event fitting:cellData.maxTextViewWidth]], + [fromThreadView.trailingAnchor constraintLessThanOrEqualToAnchor:fromThreadView.superview.trailingAnchor constant:-RoomBubbleCellLayout.reactionsViewRightMargin] + ]]; + } + } } return cell; diff --git a/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m b/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m index b0dd6b913..f258b9c3e 100644 --- a/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m +++ b/Riot/Modules/GlobalSearch/Messages/HomeMessagesSearchViewController.m @@ -156,18 +156,29 @@ } - (void)showRoomWithId:(NSString*)roomId - andEventId:(NSString*)eventId + andEvent:(MXEvent*)event inMatrixSession:(MXSession*)session { - ScreenPresentationParameters *presentationParameters = [[ScreenPresentationParameters alloc] initWithRestoreInitialDisplay:NO stackAboveVisibleViews:NO]; - + ThreadParameters *threadParameters = nil; + if (event.threadId) + { + threadParameters = [[ThreadParameters alloc] initWithThreadId:event.threadId + stackRoomScreen:NO]; + } + else if ([self.mainSession.threadingService isEventThreadRoot:event]) + { + threadParameters = [[ThreadParameters alloc] initWithThreadId:event.eventId + stackRoomScreen:NO]; + } + + ScreenPresentationParameters *screenParameters = [[ScreenPresentationParameters alloc] initWithRestoreInitialDisplay:NO stackAboveVisibleViews:NO]; + RoomNavigationParameters *parameters = [[RoomNavigationParameters alloc] initWithRoomId:roomId - eventId:eventId - mxSession:session - threadParameters:nil - presentationParameters:presentationParameters]; - - [[AppDelegate theDelegate] showRoomWithParameters:parameters]; + eventId:event.eventId + mxSession:self.mainSession + threadParameters:threadParameters + presentationParameters:screenParameters]; + [[LegacyAppDelegate theDelegate] showRoomWithParameters:parameters]; } #pragma mark - MXKDataSourceDelegate @@ -264,7 +275,7 @@ // Make the master tabBar view controller open the RoomViewController [self showRoomWithId:cellData.roomId - andEventId:_selectedEvent.eventId + andEvent:_selectedEvent inMatrixSession:cellData.mxSession]; // Reset the selected event. HomeViewController got it when here diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.h b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.h index e63d2f586..cd76e39f8 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.h +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.h @@ -50,7 +50,8 @@ The optional text pattern to be highlighted in the body of the message. */ NSString *highlightedPattern; - UIColor *highlightedPatternColor; + UIColor *highlightedPatternForegroundColor; + UIColor *highlightedPatternBackgroundColor; UIFont *highlightedPatternFont; } diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.m index b435b2f9d..0fe6bf0e0 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellData.m @@ -351,10 +351,14 @@ return customAttributedTextMsg; } -- (void)highlightPatternInTextMessage:(NSString*)pattern withForegroundColor:(UIColor*)patternColor andFont:(UIFont*)patternFont +- (void)highlightPatternInTextMessage:(NSString*)pattern + withBackgroundColor:(UIColor *)backgroundColor + foregroundColor:(UIColor*)foregroundColor + andFont:(UIFont*)patternFont { highlightedPattern = pattern; - highlightedPatternColor = patternColor; + highlightedPatternBackgroundColor = backgroundColor; + highlightedPatternForegroundColor = foregroundColor; highlightedPatternFont = patternFont; // Indicate that the text message layout should be recomputed. @@ -889,10 +893,16 @@ while (range.location != NSNotFound) { - if (highlightedPatternColor) + if (highlightedPatternBackgroundColor) + { + // Update background color + [customAttributedTextMsg addAttribute:NSBackgroundColorAttributeName value:highlightedPatternBackgroundColor range:range]; + } + + if (highlightedPatternForegroundColor) { // Update text color - [customAttributedTextMsg addAttribute:NSForegroundColorAttributeName value:highlightedPatternColor range:range]; + [customAttributedTextMsg addAttribute:NSForegroundColorAttributeName value:highlightedPatternForegroundColor range:range]; } if (highlightedPatternFont) diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellDataStoring.h b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellDataStoring.h index 06b1584c4..53b96037c 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellDataStoring.h +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomBubbleCellDataStoring.h @@ -269,10 +269,14 @@ Update the event because its sent state changed or it is has been redacted. Highlight all the occurrences of a pattern in the resulting message body 'attributedTextMessage'. @param pattern the text pattern to highlight. - @param patternColor optional text color (the pattern text color is unchanged if nil). + @param backgroundColor optional text background color (the patterns background color is unchanged if nil) + @param foregroundColor optional text color (the pattern text color is unchanged if nil). @param patternFont optional text font (the pattern font is unchanged if nil). */ -- (void)highlightPatternInTextMessage:(NSString*)pattern withForegroundColor:(UIColor*)patternColor andFont:(UIFont*)patternFont; +- (void)highlightPatternInTextMessage:(NSString*)pattern + withBackgroundColor:(UIColor *)backgroundColor + foregroundColor:(UIColor*)foregroundColor + andFont:(UIFont*)patternFont; /** Refresh the sender flair information diff --git a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m index 65607edef..ab696e63f 100644 --- a/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m +++ b/Riot/Modules/MatrixKit/Models/Room/MXKRoomDataSource.m @@ -2199,8 +2199,8 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) { [self removeEventWithEventId:eventId]; if (event.isVoiceMessage) { - NSNumber *duration = event.content[kMXMessageContentKeyExtensibleAudio][kMXMessageContentKeyExtensibleAudioDuration]; - NSArray *samples = event.content[kMXMessageContentKeyExtensibleAudio][kMXMessageContentKeyExtensibleAudioWaveform]; + NSNumber *duration = event.content[kMXMessageContentKeyExtensibleAudioMSC1767][kMXMessageContentKeyExtensibleAudioDuration]; + NSArray *samples = event.content[kMXMessageContentKeyExtensibleAudioMSC1767][kMXMessageContentKeyExtensibleAudioWaveform]; [self sendVoiceMessage:localFileURL mimeType:mimetype duration:duration.doubleValue samples:samples success:success failure:failure]; } else { diff --git a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m index ec22022c4..be5eec092 100644 --- a/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m +++ b/Riot/Modules/MatrixKit/Utils/EventFormatter/MXKEventFormatter.m @@ -1577,6 +1577,11 @@ static NSString *const kHTMLATagRegexPattern = @"([^<]*)"; } case MXEventTypePollStart: { + if (event.isEditEvent) + { + return nil; + } + displayText = [MXEventContentPollStart modelFromJSON:event.content].question; break; } diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.m b/Riot/Modules/Room/CellData/RoomBubbleCellData.m index 8602f4370..6da0487ae 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.m +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.m @@ -279,6 +279,10 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat if (self.tag == RoomBubbleCellDataTagPoll) { + if (self.events.lastObject.isEditEvent) { + return YES; + } + return NO; } @@ -297,6 +301,12 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat // do not consider this cell data if threads not enabled in the timeline return NO; } + + if (roomDataSource.threadId) + { + // do not consider this cell data if in a thread view + return NO; + } return super.hasThreadRoot; } @@ -573,6 +583,8 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat additionalVerticalHeight+= [self reactionHeightForEventId:eventId]; // Add vertical whitespace in case of a thread root additionalVerticalHeight+= [self threadSummaryViewHeightForEventId:eventId]; + // Add vertical whitespace in case of from a thread + additionalVerticalHeight+= [self fromThreadViewHeightForEventId:eventId]; // Add vertical whitespace in case of read receipts. additionalVerticalHeight+= [self readReceiptHeightForEventId:eventId]; @@ -593,6 +605,7 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat height+= [self urlPreviewHeightForEventId:eventId]; height+= [self reactionHeightForEventId:eventId]; height+= [self threadSummaryViewHeightForEventId:eventId]; + height+= [self fromThreadViewHeightForEventId:eventId]; height+= [self readReceiptHeightForEventId:eventId]; } @@ -654,7 +667,35 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat // component is not a thread root return 0; } - return RoomBubbleCellLayout.threadSummaryViewTopMargin + [ThreadSummaryView contentViewHeightForThread:component.thread fitting:self.maxTextViewWidth]; + return RoomBubbleCellLayout.threadSummaryViewTopMargin + + [ThreadSummaryView contentViewHeightForThread:component.thread fitting:self.maxTextViewWidth]; +} + +- (CGFloat)fromThreadViewHeightForEventId:(NSString*)eventId +{ + if (!RiotSettings.shared.enableThreads) + { + // do not show from thread view if threads not enabled + return 0; + } + if (roomDataSource.threadId) + { + // do not show from thread view on threads + return 0; + } + NSInteger index = [self bubbleComponentIndexForEventId:eventId]; + if (index == NSNotFound) + { + return 0; + } + MXKRoomBubbleComponent *component = self.bubbleComponents[index]; + if (!component.event.isInThread) + { + // event is not in a thread + return 0; + } + return RoomBubbleCellLayout.fromThreadViewTopMargin + + [FromThreadView contentViewHeightForEvent:component.event fitting:self.maxTextViewWidth]; } - (CGFloat)urlPreviewHeightForEventId:(NSString*)eventId diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m index bdda32a63..cebd2d2b7 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.m +++ b/Riot/Modules/Room/DataSources/RoomDataSource.m @@ -527,45 +527,55 @@ const CGFloat kTypingCellHeight = 24; { threadSummaryView = [[ThreadSummaryView alloc] initWithThread:component.thread]; threadSummaryView.delegate = self; - + [temporaryViews addObject:threadSummaryView]; [bubbleCell.tmpSubviews addObject:threadSummaryView]; - + threadSummaryView.translatesAutoresizingMaskIntoConstraints = NO; - [bubbleCell.contentView addSubview:threadSummaryView]; - - CGFloat leftMargin = RoomBubbleCellLayout.reactionsViewLeftMargin; - if (roomBubbleCellData.containsBubbleComponentWithEncryptionBadge) + + if ([[bubbleCell class] conformsToProtocol:@protocol(BubbleCellThreadSummaryDisplayable)]) { - leftMargin+= RoomBubbleCellLayout.encryptedContentLeftMargin; - } - - // The top constraint may need to include the URL preview view or reactions view - NSLayoutConstraint *topConstraint; - if (reactionsView) - { - topConstraint = [threadSummaryView.topAnchor constraintEqualToAnchor:reactionsView.bottomAnchor - constant:RoomBubbleCellLayout.threadSummaryViewTopMargin]; - } - else if (urlPreviewView) - { - topConstraint = [threadSummaryView.topAnchor constraintEqualToAnchor:urlPreviewView.bottomAnchor - constant:RoomBubbleCellLayout.threadSummaryViewTopMargin]; + id threadSummaryDisplayable = (id)bubbleCell; + + [threadSummaryDisplayable addThreadSummaryView:threadSummaryView]; } else { - topConstraint = [threadSummaryView.topAnchor constraintEqualToAnchor:threadSummaryView.superview.topAnchor - constant:bottomPositionY + RoomBubbleCellLayout.threadSummaryViewTopMargin]; + [bubbleCell.contentView addSubview:threadSummaryView]; + + CGFloat leftMargin = RoomBubbleCellLayout.reactionsViewLeftMargin; + if (roomBubbleCellData.containsBubbleComponentWithEncryptionBadge) + { + leftMargin+= RoomBubbleCellLayout.encryptedContentLeftMargin; + } + + // The top constraint may need to include the URL preview view or reactions view + NSLayoutConstraint *topConstraint; + if (reactionsView) + { + topConstraint = [threadSummaryView.topAnchor constraintEqualToAnchor:reactionsView.bottomAnchor + constant:RoomBubbleCellLayout.threadSummaryViewTopMargin]; + } + else if (urlPreviewView) + { + topConstraint = [threadSummaryView.topAnchor constraintEqualToAnchor:urlPreviewView.bottomAnchor + constant:RoomBubbleCellLayout.threadSummaryViewTopMargin]; + } + else + { + topConstraint = [threadSummaryView.topAnchor constraintEqualToAnchor:threadSummaryView.superview.topAnchor + constant:bottomPositionY + RoomBubbleCellLayout.threadSummaryViewTopMargin]; + } + + // Set constraints for the summary view + [NSLayoutConstraint activateConstraints: @[ + [threadSummaryView.leadingAnchor constraintEqualToAnchor:threadSummaryView.superview.leadingAnchor + constant:leftMargin], + topConstraint, + [threadSummaryView.heightAnchor constraintEqualToConstant:[ThreadSummaryView contentViewHeightForThread:component.thread fitting:cellData.maxTextViewWidth]], + [threadSummaryView.trailingAnchor constraintLessThanOrEqualToAnchor:threadSummaryView.superview.trailingAnchor constant:-RoomBubbleCellLayout.reactionsViewRightMargin] + ]]; } - - // Set constraints for the summary view - [NSLayoutConstraint activateConstraints: @[ - [threadSummaryView.leadingAnchor constraintEqualToAnchor:threadSummaryView.superview.leadingAnchor - constant:leftMargin], - topConstraint, - [threadSummaryView.heightAnchor constraintEqualToConstant:[ThreadSummaryView contentViewHeightForThread:component.thread fitting:cellData.maxTextViewWidth]], - [threadSummaryView.trailingAnchor constraintLessThanOrEqualToAnchor:threadSummaryView.superview.trailingAnchor constant:-RoomBubbleCellLayout.reactionsViewRightMargin] - ]]; } MXKReceiptSendersContainer* avatarsContainer; diff --git a/Riot/Modules/Room/RoomCoordinator.swift b/Riot/Modules/Room/RoomCoordinator.swift index fd076f9fb..7d71e1cb7 100644 --- a/Riot/Modules/Room/RoomCoordinator.swift +++ b/Riot/Modules/Room/RoomCoordinator.swift @@ -81,7 +81,7 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { self.activityIndicatorPresenter = ActivityIndicatorPresenter() if #available(iOS 14, *) { - PollTimelineProvider.shared.session = parameters.session + TimelinePollProvider.shared.session = parameters.session } super.init() @@ -289,6 +289,29 @@ final class RoomCoordinator: NSObject, RoomCoordinatorProtocol { navigationRouter.present(coordinator, animated: true) coordinator.start() } + + private func startEditPollCoordinator(startEvent: MXEvent? = nil) { + guard #available(iOS 14.0, *) else { + return + } + + let parameters = PollEditFormCoordinatorParameters(room: roomViewController.roomDataSource.room, pollStartEvent: startEvent) + let coordinator = PollEditFormCoordinator(parameters: parameters) + + coordinator.completion = { [weak self, weak coordinator] in + guard let self = self, let coordinator = coordinator else { + return + } + + self.navigationRouter?.dismissModule(animated: true, completion: nil) + self.remove(childCoordinator: coordinator) + } + + add(childCoordinator: coordinator) + + navigationRouter?.present(coordinator, animated: true) + coordinator.start() + } } // MARK: - RoomIdentifiable @@ -352,26 +375,7 @@ extension RoomCoordinator: RoomViewControllerDelegate { } func roomViewControllerDidRequestPollCreationFormPresentation(_ roomViewController: RoomViewController) { - guard #available(iOS 14.0, *) else { - return - } - - let parameters = PollEditFormCoordinatorParameters(room: roomViewController.roomDataSource.room) - let coordinator = PollEditFormCoordinator(parameters: parameters) - - coordinator.completion = { [weak self, weak coordinator] in - guard let self = self, let coordinator = coordinator else { - return - } - - self.navigationRouter?.dismissModule(animated: true, completion: nil) - self.remove(childCoordinator: coordinator) - } - - add(childCoordinator: coordinator) - - navigationRouter?.present(coordinator, animated: true) - coordinator.start() + startEditPollCoordinator() } func roomViewControllerDidRequestLocationSharingFormPresentation(_ roomViewController: RoomViewController) { @@ -387,7 +391,7 @@ extension RoomCoordinator: RoomViewControllerDelegate { return false } - return PollTimelineProvider.shared.pollTimelineCoordinatorForEventIdentifier(eventIdentifier)?.canEndPoll() ?? false + return TimelinePollProvider.shared.timelinePollCoordinatorForEventIdentifier(eventIdentifier)?.canEndPoll() ?? false } func roomViewController(_ roomViewController: RoomViewController, endPollWithEventIdentifier eventIdentifier: String) { @@ -395,6 +399,18 @@ extension RoomCoordinator: RoomViewControllerDelegate { return } - PollTimelineProvider.shared.pollTimelineCoordinatorForEventIdentifier(eventIdentifier)?.endPoll() + TimelinePollProvider.shared.timelinePollCoordinatorForEventIdentifier(eventIdentifier)?.endPoll() + } + + func roomViewController(_ roomViewController: RoomViewController, canEditPollWithEventIdentifier eventIdentifier: String) -> Bool { + guard #available(iOS 14.0, *) else { + return false + } + + return TimelinePollProvider.shared.timelinePollCoordinatorForEventIdentifier(eventIdentifier)?.canEditPoll() ?? false + } + + func roomViewController(_ roomViewController: RoomViewController, didRequestEditForPollWithStart startEvent: MXEvent) { + startEditPollCoordinator(startEvent: startEvent) } } diff --git a/Riot/Modules/Room/RoomDisplayConfiguration.swift b/Riot/Modules/Room/RoomDisplayConfiguration.swift index 2c3ab1ab8..1a5ec3481 100644 --- a/Riot/Modules/Room/RoomDisplayConfiguration.swift +++ b/Riot/Modules/Room/RoomDisplayConfiguration.swift @@ -24,21 +24,27 @@ class RoomDisplayConfiguration: NSObject { let integrationsEnabled: Bool let jitsiWidgetRemoverEnabled: Bool + + let sendingPollsEnabled: Bool init(callsEnabled: Bool, integrationsEnabled: Bool, - jitsiWidgetRemoverEnabled: Bool) { + jitsiWidgetRemoverEnabled: Bool, + sendingPollsEnabled: Bool) { self.callsEnabled = callsEnabled self.integrationsEnabled = integrationsEnabled self.jitsiWidgetRemoverEnabled = jitsiWidgetRemoverEnabled + self.sendingPollsEnabled = sendingPollsEnabled super.init() } static let `default`: RoomDisplayConfiguration = RoomDisplayConfiguration(callsEnabled: true, integrationsEnabled: true, - jitsiWidgetRemoverEnabled: true) + jitsiWidgetRemoverEnabled: true, + sendingPollsEnabled: true) static let forThreads: RoomDisplayConfiguration = RoomDisplayConfiguration(callsEnabled: false, integrationsEnabled: false, - jitsiWidgetRemoverEnabled: false) + jitsiWidgetRemoverEnabled: false, + sendingPollsEnabled: false) } diff --git a/Riot/Modules/Room/RoomViewController.h b/Riot/Modules/Room/RoomViewController.h index bddd31ca7..49adefb8f 100644 --- a/Riot/Modules/Room/RoomViewController.h +++ b/Riot/Modules/Room/RoomViewController.h @@ -225,6 +225,12 @@ canEndPollWithEventIdentifier:(NSString *)eventIdentifier; - (void)roomViewController:(RoomViewController *)roomViewController endPollWithEventIdentifier:(NSString *)eventIdentifier; +- (BOOL)roomViewController:(RoomViewController *)roomViewController +canEditPollWithEventIdentifier:(NSString *)eventIdentifier; + +- (void)roomViewController:(RoomViewController *)roomViewController +didRequestEditForPollWithStartEvent:(MXEvent *)startEvent; + @end NS_ASSUME_NONNULL_END diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 5933ce7b9..91c47f41e 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -1564,7 +1564,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; - (BadgedBarButtonItem *)threadListBarButtonItem { UIButton *button = [UIButton new]; - UIImage *icon = [[UIImage imageNamed:@"threads_icon"] vc_resizedWith:CGSizeMake(24, 24)]; + UIImage *icon = [[UIImage imageNamed:@"threads_icon"] vc_resizedWith:CGSizeMake(21, 21)]; button.contentEdgeInsets = UIEdgeInsetsMake(4, 8, 4, 8); [button setImage:icon forState:UIControlStateNormal]; @@ -2121,7 +2121,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [self roomInputToolbarViewDidTapFileUpload]; }]]; } - if (RiotSettings.shared.roomScreenAllowPollsAction) + if (BuildSettings.pollsEnabled && self.displayConfiguration.sendingPollsEnabled) { [actionItems addObject:[[RoomActionItem alloc] initWithImage:[UIImage imageNamed:@"action_poll"] andAction:^{ MXStrongifyAndReturnIfNil(self); @@ -3918,6 +3918,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; [actionsMenu mxk_setAccessibilityIdentifier:@"RoomVCEventMenuAlert"]; [actionsMenu popoverPresentationController].sourceView = roomBubbleTableViewCell; [actionsMenu popoverPresentationController].sourceRect = sourceRect; + [self dismissKeyboard]; [self presentViewController:actionsMenu animated:animated completion:nil]; currentAlert = actionsMenu; } @@ -6335,16 +6336,34 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05; MXWeakify(self); RoomContextualMenuItem *editMenuItem = [[RoomContextualMenuItem alloc] initWithMenuAction:RoomContextualMenuActionEdit]; - editMenuItem.action = ^{ - MXStrongifyAndReturnIfNil(self); - [self hideContextualMenuAnimated:YES cancelEventSelection:NO completion:nil]; - [self editEventContentWithId:event.eventId]; - - // And display the keyboard - [self.inputToolbarView becomeFirstResponder]; - }; - editMenuItem.isEnabled = [self.roomDataSource canEditEventWithId:event.eventId]; + switch (event.eventType) { + case MXEventTypePollStart: { + editMenuItem.action = ^{ + MXStrongifyAndReturnIfNil(self); + [self hideContextualMenuAnimated:YES cancelEventSelection:YES completion:nil]; + [self.delegate roomViewController:self didRequestEditForPollWithStartEvent:event]; + }; + + editMenuItem.isEnabled = [self.delegate roomViewController:self canEditPollWithEventIdentifier:event.eventId]; + + break; + } + default: { + editMenuItem.action = ^{ + MXStrongifyAndReturnIfNil(self); + [self hideContextualMenuAnimated:YES cancelEventSelection:NO completion:nil]; + [self editEventContentWithId:event.eventId]; + + // And display the keyboard + [self.inputToolbarView becomeFirstResponder]; + }; + + editMenuItem.isEnabled = [self.roomDataSource canEditEventWithId:event.eventId]; + + break; + } + } return editMenuItem; } diff --git a/Riot/Modules/Room/Search/DataSources/RoomSearchDataSource.m b/Riot/Modules/Room/Search/DataSources/RoomSearchDataSource.m index f35ac5c7f..58e80961e 100644 --- a/Riot/Modules/Room/Search/DataSources/RoomSearchDataSource.m +++ b/Riot/Modules/Room/Search/DataSources/RoomSearchDataSource.m @@ -69,7 +69,10 @@ if (cellData) { // Highlight the search pattern - [cellData highlightPatternInTextMessage:self.searchText withForegroundColor:ThemeService.shared.theme.tintColor andFont:patternFont]; + [cellData highlightPatternInTextMessage:self.searchText + withBackgroundColor:[UIColor clearColor] + foregroundColor:ThemeService.shared.theme.tintColor + andFont:patternFont]; // Use profile information as data to display MXSearchUserProfile *userProfile = result.context.profileInfo[result.result.sender]; @@ -91,11 +94,65 @@ if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class]) { MXKRoomBubbleTableViewCell *bubbleCell = (MXKRoomBubbleTableViewCell*)cell; - + // Display date for each message [bubbleCell addDateLabel]; + + RoomBubbleCellData *cellData = (RoomBubbleCellData*)[self cellDataAtIndex:indexPath.row]; + MXEvent *event = cellData.events.firstObject; + + if (event) + { + if (cellData.hasThreadRoot) + { + MXThread *thread = cellData.bubbleComponents.firstObject.thread; + ThreadSummaryView *threadSummaryView = [[ThreadSummaryView alloc] initWithThread:thread]; + [bubbleCell.tmpSubviews addObject:threadSummaryView]; + + threadSummaryView.translatesAutoresizingMaskIntoConstraints = NO; + [bubbleCell.contentView addSubview:threadSummaryView]; + + CGFloat leftMargin = RoomBubbleCellLayout.reactionsViewLeftMargin; + + CGRect bubbleComponentFrame = [bubbleCell componentFrameInContentViewForIndex:0]; + CGFloat bottomPositionY = bubbleComponentFrame.origin.y + bubbleComponentFrame.size.height; + + // Set constraints for the summary view + [NSLayoutConstraint activateConstraints: @[ + [threadSummaryView.leadingAnchor constraintEqualToAnchor:threadSummaryView.superview.leadingAnchor + constant:leftMargin], + [threadSummaryView.topAnchor constraintEqualToAnchor:threadSummaryView.superview.topAnchor + constant:bottomPositionY + RoomBubbleCellLayout.threadSummaryViewTopMargin], + [threadSummaryView.heightAnchor constraintEqualToConstant:[ThreadSummaryView contentViewHeightForThread:thread fitting:cellData.maxTextViewWidth]], + [threadSummaryView.trailingAnchor constraintLessThanOrEqualToAnchor:threadSummaryView.superview.trailingAnchor constant:-RoomBubbleCellLayout.reactionsViewRightMargin] + ]]; + } + else if (event.isInThread) + { + FromThreadView *fromThreadView = [FromThreadView instantiate]; + [bubbleCell.tmpSubviews addObject:fromThreadView]; + + fromThreadView.translatesAutoresizingMaskIntoConstraints = NO; + [bubbleCell.contentView addSubview:fromThreadView]; + + CGFloat leftMargin = RoomBubbleCellLayout.reactionsViewLeftMargin; + + CGRect bubbleComponentFrame = [bubbleCell componentFrameInContentViewForIndex:0]; + CGFloat bottomPositionY = bubbleComponentFrame.origin.y + bubbleComponentFrame.size.height; + + // Set constraints for the summary view + [NSLayoutConstraint activateConstraints: @[ + [fromThreadView.leadingAnchor constraintEqualToAnchor:fromThreadView.superview.leadingAnchor + constant:leftMargin], + [fromThreadView.topAnchor constraintEqualToAnchor:fromThreadView.superview.topAnchor + constant:bottomPositionY + RoomBubbleCellLayout.fromThreadViewTopMargin], + [fromThreadView.heightAnchor constraintEqualToConstant:[FromThreadView contentViewHeightForEvent:event fitting:cellData.maxTextViewWidth]], + [fromThreadView.trailingAnchor constraintLessThanOrEqualToAnchor:fromThreadView.superview.trailingAnchor constant:-RoomBubbleCellLayout.reactionsViewRightMargin] + ]]; + } + } } - + return cell; } diff --git a/Riot/Modules/Room/Views/BubbleCells/BaseBubbleCell/BaseBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/BaseBubbleCell/BaseBubbleCell.swift index 6c0db2280..02a92cb87 100644 --- a/Riot/Modules/Room/Views/BubbleCells/BaseBubbleCell/BaseBubbleCell.swift +++ b/Riot/Modules/Room/Views/BubbleCells/BaseBubbleCell/BaseBubbleCell.swift @@ -168,6 +168,10 @@ class BaseBubbleCell: MXKRoomBubbleTableViewCell, BaseBubbleCellType { if let bubbleCellReactionsDisplayable = self as? BubbleCellReactionsDisplayable { bubbleCellReactionsDisplayable.removeReactionsView() } + + if let bubbleCellThreadSummaryDisplayable = self as? BubbleCellThreadSummaryDisplayable { + bubbleCellThreadSummaryDisplayable.removeThreadSummaryView() + } } override func render(_ cellData: MXKCellData!) { @@ -244,6 +248,16 @@ class BaseBubbleCell: MXKRoomBubbleTableViewCell, BaseBubbleCellType { func removeReactionsView() { self.bubbleCellContentView?.removeReactionsView() } + + // MARK: - BubbleCellThreadSummaryDisplayable + + func addThreadSummaryView(_ threadSummaryView: ThreadSummaryView) { + self.bubbleCellContentView?.addThreadSummaryView(threadSummaryView) + } + + func removeThreadSummaryView() { + self.bubbleCellContentView?.removeThreadSummaryView() + } // Encryption status diff --git a/Riot/Modules/Room/Views/BubbleCells/BaseBubbleCell/BubbleCellContentView.swift b/Riot/Modules/Room/Views/BubbleCells/BaseBubbleCell/BubbleCellContentView.swift index 22597a4f2..5a15f64d5 100644 --- a/Riot/Modules/Room/Views/BubbleCells/BaseBubbleCell/BubbleCellContentView.swift +++ b/Riot/Modules/Room/Views/BubbleCells/BaseBubbleCell/BubbleCellContentView.swift @@ -47,6 +47,8 @@ final class BubbleCellContentView: UIView, NibLoadable { @IBOutlet weak var reactionsContainerView: UIView! @IBOutlet weak var reactionsContentView: UIView! + + @IBOutlet weak var threadSummaryContainerView: UIView! @IBOutlet weak var bubbleOverlayContainer: UIView! @@ -69,6 +71,14 @@ final class BubbleCellContentView: UIView, NibLoadable { self.reactionsContainerView.isHidden = !newValue } } + + private var showThreadSummary: Bool { + get { + return !self.threadSummaryContainerView.isHidden + } set { + self.threadSummaryContainerView.isHidden = !newValue + } + } // MARK: Public @@ -143,3 +153,27 @@ extension BubbleCellContentView: BubbleCellReactionsDisplayable { self.reactionsContentView.vc_removeAllSubviews() } } + +// MARK: - BubbleCellThreadSummaryDisplayable +extension BubbleCellContentView: BubbleCellThreadSummaryDisplayable { + + func addThreadSummaryView(_ threadSummaryView: ThreadSummaryView) { + self.threadSummaryContainerView.vc_removeAllSubviews() + self.threadSummaryContainerView.addSubview(threadSummaryView) + NSLayoutConstraint.activate([ + threadSummaryView.leadingAnchor.constraint(equalTo: innerContentView.leadingAnchor), + threadSummaryView.topAnchor.constraint(equalTo: threadSummaryContainerView.topAnchor), + threadSummaryView.heightAnchor.constraint(equalToConstant: RoomBubbleCellLayout.threadSummaryViewHeight), + threadSummaryView.bottomAnchor.constraint(equalTo: threadSummaryContainerView.bottomAnchor), + threadSummaryView.trailingAnchor.constraint(lessThanOrEqualTo: threadSummaryContainerView.trailingAnchor, + constant: -RoomBubbleCellLayout.reactionsViewRightMargin) + ]) + self.showThreadSummary = true + } + + func removeThreadSummaryView() { + self.showThreadSummary = false + self.threadSummaryContainerView.vc_removeAllSubviews() + } + +} diff --git a/Riot/Modules/Room/Views/BubbleCells/BaseBubbleCell/BubbleCellContentView.xib b/Riot/Modules/Room/Views/BubbleCells/BaseBubbleCell/BubbleCellContentView.xib index adbd1b79a..1a5a30708 100644 --- a/Riot/Modules/Room/Views/BubbleCells/BaseBubbleCell/BubbleCellContentView.xib +++ b/Riot/Modules/Room/Views/BubbleCells/BaseBubbleCell/BubbleCellContentView.xib @@ -195,6 +195,10 @@ + @@ -238,6 +242,7 @@ + diff --git a/Riot/Modules/Room/Views/BubbleCells/BaseBubbleCell/BubbleCellThreadSummaryDisplayable.swift b/Riot/Modules/Room/Views/BubbleCells/BaseBubbleCell/BubbleCellThreadSummaryDisplayable.swift new file mode 100644 index 000000000..6c2848e27 --- /dev/null +++ b/Riot/Modules/Room/Views/BubbleCells/BaseBubbleCell/BubbleCellThreadSummaryDisplayable.swift @@ -0,0 +1,23 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +/// `BubbleCellThreadSummaryDisplayable` is a protocol indicating that a cell support displaying a thread summary. +@objc protocol BubbleCellThreadSummaryDisplayable { + func addThreadSummaryView(_ threadSummaryView: ThreadSummaryView) + func removeThreadSummaryView() +} diff --git a/Riot/Modules/Room/Views/BubbleCells/Encryption/RoomBubbleCellLayout.swift b/Riot/Modules/Room/Views/BubbleCells/Encryption/RoomBubbleCellLayout.swift index a860d942b..461da960b 100644 --- a/Riot/Modules/Room/Views/BubbleCells/Encryption/RoomBubbleCellLayout.swift +++ b/Riot/Modules/Room/Views/BubbleCells/Encryption/RoomBubbleCellLayout.swift @@ -50,4 +50,6 @@ final class RoomBubbleCellLayout: NSObject { // Threads static let threadSummaryViewTopMargin: CGFloat = 8.0 + static let threadSummaryViewHeight: CGFloat = 40.0 + static let fromThreadViewTopMargin: CGFloat = 8.0 } diff --git a/Riot/Modules/Room/Views/BubbleCells/Poll/PollBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/Poll/PollBubbleCell.swift index 4d3d42e28..671adb2e9 100644 --- a/Riot/Modules/Room/Views/BubbleCells/Poll/PollBubbleCell.swift +++ b/Riot/Modules/Room/Views/BubbleCells/Poll/PollBubbleCell.swift @@ -29,7 +29,7 @@ class PollBubbleCell: SizableBaseBubbleCell, BubbleCellReactionsDisplayable { let bubbleData = cellData as? RoomBubbleCellData, let event = bubbleData.events.last, event.eventType == __MXEventType.pollStart, - let view = PollTimelineProvider.shared.buildPollTimelineViewForEvent(event) else { + let view = TimelinePollProvider.shared.buildTimelinePollViewForEvent(event) else { return } @@ -57,3 +57,5 @@ class PollBubbleCell: SizableBaseBubbleCell, BubbleCellReactionsDisplayable { delegate.cell(self, didRecognizeAction: kMXKRoomBubbleCellTapOnContentView, userInfo: [kMXKRoomBubbleCellEventKey: event]) } } + +extension PollBubbleCell: BubbleCellThreadSummaryDisplayable {} diff --git a/Riot/Modules/Room/Views/BubbleCells/SizableCell/SizableBubbleCell.swift b/Riot/Modules/Room/Views/BubbleCells/SizableCell/SizableBubbleCell.swift index d7f00f0d7..9c802f469 100644 --- a/Riot/Modules/Room/Views/BubbleCells/SizableCell/SizableBubbleCell.swift +++ b/Riot/Modules/Room/Views/BubbleCells/SizableCell/SizableBubbleCell.swift @@ -130,6 +130,13 @@ class SizableBaseBubbleCell: BaseBubbleCell, SizableBaseBubbleCellType { let reactionsHeight = self.reactionsViewSizer.height(for: bubbleReactionsViewModel, fittingWidth: reactionWidth) height+=reactionsHeight } + + // Add thread summary view height if needed + if sizingView is BubbleCellThreadSummaryDisplayable, + let roomBubbleCellData = cellData as? RoomBubbleCellData, + roomBubbleCellData.hasThreadRoot { + height += RoomBubbleCellLayout.threadSummaryViewHeight + } return height } diff --git a/Riot/Modules/Room/Views/Threads/From/FromThreadView.swift b/Riot/Modules/Room/Views/Threads/From/FromThreadView.swift new file mode 100644 index 000000000..992a8b202 --- /dev/null +++ b/Riot/Modules/Room/Views/Threads/From/FromThreadView.swift @@ -0,0 +1,54 @@ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation +import Reusable + +@objcMembers +class FromThreadView: UIView { + + private enum Constants { + static let viewHeight: CGFloat = 18 + } + + @IBOutlet private weak var iconView: UIImageView! + @IBOutlet private weak var titleLabel: UILabel! + + static func contentViewHeight(forEvent event: MXEvent, + fitting maxWidth: CGFloat) -> CGFloat { + return Constants.viewHeight + } + + static func instantiate() -> FromThreadView { + let view = FromThreadView.loadFromNib() + view.update(theme: ThemeService.shared().theme) + view.titleLabel.text = VectorL10n.messageFromAThread + return view + } + +} + +extension FromThreadView: NibLoadable {} + +extension FromThreadView: Themable { + + func update(theme: Theme) { + backgroundColor = .clear + iconView.tintColor = theme.colors.secondaryContent + titleLabel.textColor = theme.colors.secondaryContent + } + +} diff --git a/Riot/Modules/Room/Views/Threads/From/FromThreadView.xib b/Riot/Modules/Room/Views/Threads/From/FromThreadView.xib new file mode 100644 index 000000000..8e8e9f8b2 --- /dev/null +++ b/Riot/Modules/Room/Views/Threads/From/FromThreadView.xib @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Riot/Modules/Room/Views/Threads/ThreadSummaryView.swift b/Riot/Modules/Room/Views/Threads/Summary/ThreadSummaryView.swift similarity index 96% rename from Riot/Modules/Room/Views/Threads/ThreadSummaryView.swift rename to Riot/Modules/Room/Views/Threads/Summary/ThreadSummaryView.swift index 8ed8f74ae..61917e30e 100644 --- a/Riot/Modules/Room/Views/Threads/ThreadSummaryView.swift +++ b/Riot/Modules/Room/Views/Threads/Summary/ThreadSummaryView.swift @@ -27,9 +27,8 @@ protocol ThreadSummaryViewDelegate: AnyObject { class ThreadSummaryView: UIView { private enum Constants { - static let viewHeight: CGFloat = 40 static let viewDefaultWidth: CGFloat = 320 - static let cornerRadius: CGFloat = 4 + static let cornerRadius: CGFloat = 8 static let lastMessageFont: UIFont = .systemFont(ofSize: 13) } @@ -53,14 +52,14 @@ class ThreadSummaryView: UIView { self.thread = thread super.init(frame: CGRect(origin: .zero, size: CGSize(width: Constants.viewDefaultWidth, - height: Constants.viewHeight))) + height: RoomBubbleCellLayout.threadSummaryViewHeight))) loadNibContent() update(theme: ThemeService.shared().theme) configure() } static func contentViewHeight(forThread thread: MXThread, fitting maxWidth: CGFloat) -> CGFloat { - return Constants.viewHeight + return RoomBubbleCellLayout.threadSummaryViewHeight } required init?(coder: NSCoder) { diff --git a/Riot/Modules/Room/Views/Threads/ThreadSummaryView.xib b/Riot/Modules/Room/Views/Threads/Summary/ThreadSummaryView.xib similarity index 88% rename from Riot/Modules/Room/Views/Threads/ThreadSummaryView.xib rename to Riot/Modules/Room/Views/Threads/Summary/ThreadSummaryView.xib index 12f97e11c..94a4f1166 100644 --- a/Riot/Modules/Room/Views/Threads/ThreadSummaryView.xib +++ b/Riot/Modules/Room/Views/Threads/Summary/ThreadSummaryView.xib @@ -25,14 +25,15 @@ - + - + + - + - + + - - - + + - @@ -87,6 +87,6 @@ - + diff --git a/Riot/Modules/Room/Views/Threads/ThreadSummaryViewModel.swift b/Riot/Modules/Room/Views/Threads/Summary/ThreadSummaryViewModel.swift similarity index 100% rename from Riot/Modules/Room/Views/Threads/ThreadSummaryViewModel.swift rename to Riot/Modules/Room/Views/Threads/Summary/ThreadSummaryViewModel.swift diff --git a/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.xib b/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.xib index 577c14329..08667bfdc 100644 --- a/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.xib +++ b/Riot/Modules/Room/Views/Title/Thread/ThreadRoomTitleView.xib @@ -16,31 +16,31 @@ - + - + - + - + @@ -59,7 +59,7 @@ - + @@ -72,7 +72,7 @@ - + @@ -80,7 +80,7 @@ - + diff --git a/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadViewModel.swift b/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadViewModel.swift index 34170f71f..d4b456ce1 100644 --- a/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadViewModel.swift +++ b/Riot/Modules/Threads/ThreadList/Views/Cell/ThreadViewModel.swift @@ -21,6 +21,7 @@ struct ThreadViewModel { var rootMessageSenderAvatar: AvatarViewDataProtocol? var rootMessageSenderDisplayName: String? var rootMessageText: NSAttributedString? + var rootMessageRedacted: Bool var lastMessageTime: String? var summaryViewModel: ThreadSummaryViewModel? var notificationStatus: ThreadNotificationStatus diff --git a/RiotNSE/NotificationService.swift b/RiotNSE/NotificationService.swift index c77a9dd86..b43eb8d7b 100644 --- a/RiotNSE/NotificationService.swift +++ b/RiotNSE/NotificationService.swift @@ -427,7 +427,7 @@ class NotificationService: UNNotificationServiceExtension { if event.isReply() { let parser = MXReplyEventParser() let replyParts = parser.parse(event) - notificationBody = replyParts.bodyParts.replyText + notificationBody = replyParts?.bodyParts.replyText } else { notificationBody = messageContent } diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptModels.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptModels.swift index b9678fdbd..99f96bce8 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptModels.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptModels.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh AnalyticsPrompt AnalyticsPrompt // // Copyright 2021 New Vector Ltd // diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptViewModel.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptViewModel.swift index 999e2a95f..971929ab4 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptViewModel.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/AnalyticsPromptViewModel.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh AnalyticsPrompt AnalyticsPrompt // // Copyright 2021 New Vector Ltd // diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptCoordinator.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptCoordinator.swift index c9e5f44b4..6e107b62b 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptCoordinator.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/Coordinator/AnalyticsPromptCoordinator.swift @@ -1,20 +1,18 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh AnalyticsPrompt AnalyticsPrompt -/* - 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. - */ +// +// 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 SwiftUI diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptScreenState.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptScreenState.swift index 9c303bbbe..ed947e303 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptScreenState.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/MockAnalyticsPromptScreenState.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh AnalyticsPrompt AnalyticsPrompt // // Copyright 2021 New Vector Ltd // diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/Test/UI/AnalyticsPromptUITests.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/Test/UI/AnalyticsPromptUITests.swift index b8a38a117..c24e1fa63 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/Test/UI/AnalyticsPromptUITests.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/Test/UI/AnalyticsPromptUITests.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh AnalyticsPrompt AnalyticsPrompt // // Copyright 2021 New Vector Ltd // diff --git a/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPrompt.swift b/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPrompt.swift index 8f7acf49d..5e622ab5d 100644 --- a/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPrompt.swift +++ b/RiotSwiftUI/Modules/AnalyticsPrompt/View/AnalyticsPrompt.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh AnalyticsPrompt AnalyticsPrompt // // Copyright 2021 New Vector Ltd // diff --git a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift index f64a1a51f..cedc46d0f 100644 --- a/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift +++ b/RiotSwiftUI/Modules/Common/Mock/MockAppScreens.swift @@ -24,7 +24,7 @@ enum MockAppScreens { MockAnalyticsPromptScreenState.self, MockUserSuggestionScreenState.self, MockPollEditFormScreenState.self, - MockPollTimelineScreenState.self, + MockTimelinePollScreenState.self, MockTemplateUserProfileScreenState.self, MockTemplateRoomListScreenState.self, MockTemplateRoomChatScreenState.self diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift index be9c188f9..88b0ee9cb 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingModels.swift @@ -66,11 +66,11 @@ struct LocationSharingViewState: BindableState { } struct LocationSharingViewStateBindings { - var alertInfo: ErrorAlertInfo? + var alertInfo: LocationSharingErrorAlertInfo? var userLocation: CLLocationCoordinate2D? } -struct ErrorAlertInfo: Identifiable { +struct LocationSharingErrorAlertInfo: Identifiable { enum AlertType { case mapLoadingError case userLocatingError diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingScreenState.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingScreenState.swift index f316d761e..386471b02 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingScreenState.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingScreenState.swift @@ -25,7 +25,7 @@ enum MockLocationSharingScreenState: MockScreenState, CaseIterable { case displayExistingLocation var screenType: Any.Type { - MockLocationSharingScreenState.self + LocationSharingView.self } var screenView: ([Any], AnyView) { diff --git a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift index 765c0b558..40b750f85 100644 --- a/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift +++ b/RiotSwiftUI/Modules/Room/LocationSharing/LocationSharingViewModel.swift @@ -72,24 +72,24 @@ class LocationSharingViewModel: LocationSharingViewModelType { switch error { case .failedLoadingMap: - state.bindings.alertInfo = ErrorAlertInfo(id: .mapLoadingError, - title: VectorL10n.locationSharingLoadingMapErrorTitle(AppInfo.current.displayName) , - primaryButton: (VectorL10n.ok, { completion?(.cancel) }), - secondaryButton: nil) + state.bindings.alertInfo = LocationSharingErrorAlertInfo(id: .mapLoadingError, + title: VectorL10n.locationSharingLoadingMapErrorTitle(AppInfo.current.displayName) , + primaryButton: (VectorL10n.ok, { completion?(.cancel) }), + secondaryButton: nil) case .failedLocatingUser: - state.bindings.alertInfo = ErrorAlertInfo(id: .userLocatingError, - title: VectorL10n.locationSharingLocatingUserErrorTitle(AppInfo.current.displayName), - primaryButton: (VectorL10n.ok, { completion?(.cancel) }), - secondaryButton: nil) + state.bindings.alertInfo = LocationSharingErrorAlertInfo(id: .userLocatingError, + title: VectorL10n.locationSharingLocatingUserErrorTitle(AppInfo.current.displayName), + primaryButton: (VectorL10n.ok, { completion?(.cancel) }), + secondaryButton: nil) case .invalidLocationAuthorization: - state.bindings.alertInfo = ErrorAlertInfo(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) - } - })) + 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 } @@ -100,10 +100,10 @@ class LocationSharingViewModel: LocationSharingViewModelType { state.showLoadingIndicator = false if error != nil { - state.bindings.alertInfo = ErrorAlertInfo(id: .locationSharingError, - title: VectorL10n.locationSharingInvalidAuthorizationErrorTitle(AppInfo.current.displayName), - primaryButton: (VectorL10n.ok, nil), - secondaryButton: nil) + state.bindings.alertInfo = LocationSharingErrorAlertInfo(id: .locationSharingError, + title: VectorL10n.locationSharingInvalidAuthorizationErrorTitle(AppInfo.current.displayName), + primaryButton: (VectorL10n.ok, nil), + secondaryButton: nil) } } } diff --git a/RiotSwiftUI/Modules/Room/NotificationSettings/Coordinator/RoomNotificationSettingsCoordinator.swift b/RiotSwiftUI/Modules/Room/NotificationSettings/Coordinator/RoomNotificationSettingsCoordinator.swift index 6d79f5e67..b150c5bb4 100644 --- a/RiotSwiftUI/Modules/Room/NotificationSettings/Coordinator/RoomNotificationSettingsCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/NotificationSettings/Coordinator/RoomNotificationSettingsCoordinator.swift @@ -1,20 +1,18 @@ -// File created from ScreenTemplate -// $ createScreen.sh Room/NotificationSettings RoomNotificationSettings -/* - 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. - */ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// import Foundation import UIKit diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift b/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift index 0bc35efc8..7476ac01a 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/Coordinator/PollEditFormCoordinator.swift @@ -1,20 +1,18 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/PollEditForm PollEditForm -/* - 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. - */ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// import Foundation import UIKit @@ -22,6 +20,7 @@ import SwiftUI struct PollEditFormCoordinatorParameters { let room: MXRoom + let pollStartEvent: MXEvent? } final class PollEditFormCoordinator: Coordinator, Presentable { @@ -40,7 +39,7 @@ final class PollEditFormCoordinator: Coordinator, Presentable { } // MARK: Public - + var childCoordinators: [Coordinator] = [] var completion: (() -> Void)? @@ -51,9 +50,20 @@ final class PollEditFormCoordinator: Coordinator, Presentable { init(parameters: PollEditFormCoordinatorParameters) { self.parameters = parameters - let viewModel = PollEditFormViewModel() - let view = PollEditForm(viewModel: viewModel.context) + var viewModel: PollEditFormViewModel + if let startEvent = parameters.pollStartEvent, + let pollContent = MXEventContentPollStart(fromJSON: startEvent.content) { + viewModel = PollEditFormViewModel(parameters: PollEditFormViewModelParameters(mode: .editing, + pollDetails: EditFormPollDetails(type: Self.pollKindKeyToDetailsType(pollContent.kind), + question: pollContent.question, + answerOptions: pollContent.answerOptions.map { $0.text }))) + } else { + viewModel = PollEditFormViewModel(parameters: PollEditFormViewModelParameters(mode: .creation, pollDetails: .default)) + } + + let view = PollEditForm(viewModel: viewModel.context) + _pollEditFormViewModel = viewModel pollEditFormHostingController = VectorHostingController(rootView: view) } @@ -70,16 +80,9 @@ final class PollEditFormCoordinator: Coordinator, Presentable { switch result { case .cancel: self.completion?() - case .create(let question, let answerOptions): - var options = [MXEventContentPollStartAnswerOption]() - for answerOption in answerOptions { - options.append(MXEventContentPollStartAnswerOption(uuid: UUID().uuidString, text: answerOption)) - } + case .create(let details): - let pollStartContent = MXEventContentPollStart(question: question, - kind: kMXMessageContentKeyExtensiblePollKindDisclosed, - maxSelections: 1, - answerOptions: options) + let pollStartContent = self.buildPollContentWithDetails(details) self.pollEditFormViewModel.dispatch(action: .startLoading) @@ -92,15 +95,72 @@ final class PollEditFormCoordinator: Coordinator, Presentable { guard let self = self else { return } MXLog.error("Failed creating poll with error: \(String(describing: error))") - self.pollEditFormViewModel.dispatch(action: .stopLoading(error)) + self.pollEditFormViewModel.dispatch(action: .stopLoading(.failedCreatingPoll)) } + + case .update(let details): + guard let pollStartEvent = self.parameters.pollStartEvent else { + fatalError() + } + + self.pollEditFormViewModel.dispatch(action: .startLoading) + + guard let oldPollContent = MXEventContentPollStart(fromJSON: pollStartEvent.content) else { + self.pollEditFormViewModel.dispatch(action: .stopLoading(.failedUpdatingPoll)) + return + } + + let newPollContent = self.buildPollContentWithDetails(details) + + self.parameters.room.sendPollUpdate(for: pollStartEvent, + oldContent: oldPollContent, + newContent: newPollContent, localEcho: nil) { [weak self] result in + guard let self = self else { return } + + self.pollEditFormViewModel.dispatch(action: .stopLoading(nil)) + 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)) + } } } } - // MARK: - Private + // MARK: - Presentable func toPresentable() -> UIViewController { return pollEditFormHostingController } + + // MARK: - Private + + private func buildPollContentWithDetails(_ details: EditFormPollDetails) -> MXEventContentPollStart { + var options = [MXEventContentPollStartAnswerOption]() + for answerOption in details.answerOptions { + options.append(MXEventContentPollStartAnswerOption(uuid: UUID().uuidString, text: answerOption)) + } + + return MXEventContentPollStart(question: details.question, + kind: Self.pollDetailsTypeToKindKey(details.type), + maxSelections: NSNumber(value: details.maxSelections), + answerOptions: options) + + } + + private static func pollDetailsTypeToKindKey(_ type: EditFormPollType) -> String { + let mapping = [EditFormPollType.disclosed : kMXMessageContentKeyExtensiblePollKindDisclosed, + EditFormPollType.undisclosed : kMXMessageContentKeyExtensiblePollKindUndisclosed] + + return mapping[type] ?? kMXMessageContentKeyExtensiblePollKindDisclosed + } + + private static func pollKindKeyToDetailsType(_ key: String) -> EditFormPollType { + let mapping = [kMXMessageContentKeyExtensiblePollKindDisclosed : EditFormPollType.disclosed, + kMXMessageContentKeyExtensiblePollKindUndisclosed : EditFormPollType.undisclosed] + + return mapping[key] ?? EditFormPollType.disclosed + } } diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormModels.swift b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormModels.swift index 9c7d856c8..346ba3d57 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormModels.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormModels.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/PollEditForm PollEditForm // // Copyright 2021 New Vector Ltd // @@ -19,10 +17,31 @@ import Foundation import SwiftUI +enum EditFormPollType { + case disclosed + case undisclosed +} + +struct EditFormPollDetails { + let type: EditFormPollType + let question: String + let answerOptions: [String] + let maxSelections: UInt = 1 + + static var `default`: EditFormPollDetails { + EditFormPollDetails(type: .disclosed, question: "", answerOptions: ["", ""]) + } +} + +enum PollEditFormMode { + case creation + case editing +} + enum PollEditFormStateAction { case viewAction(PollEditFormViewAction) case startLoading - case stopLoading(Error?) + case stopLoading(PollEditFormErrorAlertInfo.AlertType?) } enum PollEditFormViewAction { @@ -30,11 +49,13 @@ enum PollEditFormViewAction { case deleteAnswerOption(PollEditFormAnswerOption) case cancel case create + case update } enum PollEditFormViewModelResult { case cancel - case create(String, [String]) + case create(EditFormPollDetails) + case update(EditFormPollDetails) } struct PollEditFormQuestion { @@ -60,12 +81,14 @@ struct PollEditFormAnswerOption: Identifiable, Equatable { } struct PollEditFormViewState: BindableState { + var minAnswerOptionsCount: Int var maxAnswerOptionsCount: Int + var mode: PollEditFormMode var bindings: PollEditFormViewStateBindings var confirmationButtonEnabled: Bool { !bindings.question.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && - bindings.answerOptions.filter({ !$0.text.isEmpty }).count >= 2 + bindings.answerOptions.filter({ !$0.text.isEmpty }).count >= minAnswerOptionsCount } var addAnswerOptionButtonEnabled: Bool { @@ -78,6 +101,18 @@ struct PollEditFormViewState: BindableState { struct PollEditFormViewStateBindings { var question: PollEditFormQuestion var answerOptions: [PollEditFormAnswerOption] + var type: EditFormPollType - var showsFailureAlert: Bool = false + var alertInfo: PollEditFormErrorAlertInfo? +} + +struct PollEditFormErrorAlertInfo: Identifiable { + enum AlertType { + case failedCreatingPoll + case failedUpdatingPoll + } + + let id: AlertType + let title: String + let subtitle: String } diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormScreenState.swift b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormScreenState.swift index 2e545a9d4..8d6720340 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormScreenState.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormScreenState.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/UserSuggestion UserSuggestion // // Copyright 2021 New Vector Ltd // @@ -24,11 +22,11 @@ enum MockPollEditFormScreenState: MockScreenState, CaseIterable { case standard var screenType: Any.Type { - MockPollEditFormScreenState.self + PollEditForm.self } var screenView: ([Any], AnyView) { - let viewModel = PollEditFormViewModel() + let viewModel = PollEditFormViewModel(parameters: PollEditFormViewModelParameters(mode: .creation, pollDetails: .default)) return ([viewModel], AnyView(PollEditForm(viewModel: viewModel.context))) } } diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormViewModel.swift b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormViewModel.swift index f38ac9fc9..022b4b727 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormViewModel.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/PollEditFormViewModel.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/PollEditForm PollEditForm // // Copyright 2021 New Vector Ltd // @@ -19,6 +17,11 @@ import SwiftUI import Combine +struct PollEditFormViewModelParameters { + let mode: PollEditFormMode + let pollDetails: EditFormPollDetails +} + @available(iOS 14, *) typealias PollEditFormViewModelType = StateStoreViewModel< PollEditFormViewState, PollEditFormStateAction, @@ -27,6 +30,7 @@ typealias PollEditFormViewModelType = StateStoreViewModel< PollEditFormViewState class PollEditFormViewModel: PollEditFormViewModelType { private struct Constants { + static let minAnswerOptionsCount = 2 static let maxAnswerOptionsCount = 20 static let maxQuestionLength = 340 static let maxAnswerOptionLength = 340 @@ -42,20 +46,19 @@ class PollEditFormViewModel: PollEditFormViewModelType { // MARK: - Setup - init() { - super.init(initialViewState: Self.defaultState()) - } - - private static func defaultState() -> PollEditFormViewState { - return PollEditFormViewState( + init(parameters: PollEditFormViewModelParameters) { + let state = PollEditFormViewState( + minAnswerOptionsCount: Constants.minAnswerOptionsCount, maxAnswerOptionsCount: Constants.maxAnswerOptionsCount, + mode: parameters.mode, bindings: PollEditFormViewStateBindings( - question: PollEditFormQuestion(text: "", maxLength: Constants.maxQuestionLength), - answerOptions: [PollEditFormAnswerOption(text: "", maxLength: Constants.maxAnswerOptionLength), - PollEditFormAnswerOption(text: "", maxLength: Constants.maxAnswerOptionLength) - ] + question: PollEditFormQuestion(text: parameters.pollDetails.question, maxLength: Constants.maxQuestionLength), + answerOptions: parameters.pollDetails.answerOptions.map { PollEditFormAnswerOption(text: $0, maxLength: Constants.maxAnswerOptionLength) }, + type: parameters.pollDetails.type ) ) + + super.init(initialViewState: state) } // MARK: - Public @@ -65,11 +68,9 @@ class PollEditFormViewModel: PollEditFormViewModelType { case .cancel: completion?(.cancel) case .create: - completion?(.create(state.bindings.question.text.trimmingCharacters(in: .whitespacesAndNewlines), - state.bindings.answerOptions.compactMap({ answerOption in - let text = answerOption.text.trimmingCharacters(in: .whitespacesAndNewlines) - return text.isEmpty ? nil : text - }))) + completion?(.create(buildPollDetails())) + case .update: + completion?(.update(buildPollDetails())) default: dispatch(action: .viewAction(viewAction)) } @@ -92,10 +93,30 @@ class PollEditFormViewModel: PollEditFormViewModelType { case .stopLoading(let error): state.showLoadingIndicator = false - if error != nil { - state.bindings.showsFailureAlert = true + 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 } break } } + + // MARK: - Private + + private func buildPollDetails() -> EditFormPollDetails { + 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 + })) + } } diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/Test/UI/PollEditFormUITests.swift b/RiotSwiftUI/Modules/Room/PollEditForm/Test/UI/PollEditFormUITests.swift index 2780739cd..70042a696 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/Test/UI/PollEditFormUITests.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/Test/UI/PollEditFormUITests.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/PollEditForm PollEditForm // // Copyright 2021 New Vector Ltd // diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/Test/Unit/PollEditFormViewModelTests.swift b/RiotSwiftUI/Modules/Room/PollEditForm/Test/Unit/PollEditFormViewModelTests.swift index 739361197..662f12c1a 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/Test/Unit/PollEditFormViewModelTests.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/Test/Unit/PollEditFormViewModelTests.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/PollEditForm PollEditForm // // Copyright 2021 New Vector Ltd // @@ -28,10 +26,10 @@ class PollEditFormViewModelTests: XCTestCase { var cancellables = Set() override func setUpWithError() throws { - viewModel = PollEditFormViewModel() + viewModel = PollEditFormViewModel(parameters: PollEditFormViewModelParameters(mode: .creation, pollDetails: .default)) context = viewModel.context } - + func testInitialState() { XCTAssertTrue(context.question.text.isEmpty) XCTAssertFalse(context.viewState.confirmationButtonEnabled) @@ -100,14 +98,14 @@ class PollEditFormViewModelTests: XCTestCase { let thirdAnswer = " " viewModel.completion = { result in - if case PollEditFormViewModelResult.create(let resultQuestion, let resultAnswerOptions) = result { - XCTAssertEqual(question.trimmingCharacters(in: .whitespacesAndNewlines), resultQuestion) + if case PollEditFormViewModelResult.create(let result) = result { + XCTAssertEqual(question.trimmingCharacters(in: .whitespacesAndNewlines), result.question) // The last answer option should be automatically dropped as it's empty - XCTAssertEqual(resultAnswerOptions.count, 2) + XCTAssertEqual(result.answerOptions.count, 2) - XCTAssertEqual(resultAnswerOptions[0], firstAnswer.trimmingCharacters(in: .whitespacesAndNewlines)) - XCTAssertEqual(resultAnswerOptions[1], secondAnswer.trimmingCharacters(in: .whitespacesAndNewlines)) + XCTAssertEqual(result.answerOptions[0], firstAnswer.trimmingCharacters(in: .whitespacesAndNewlines)) + XCTAssertEqual(result.answerOptions[1], secondAnswer.trimmingCharacters(in: .whitespacesAndNewlines)) } } diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift b/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift index 57f3a63a3..c4f4608e2 100644 --- a/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift +++ b/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditForm.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/PollEditForm PollEditForm // // Copyright 2021 New Vector Ltd // @@ -37,6 +35,9 @@ struct PollEditForm: View { ScrollView { VStack(alignment: .leading, spacing: 32.0) { + // Intentionally disabled until platform parity. + // PollEditFormTypePicker(selectedType: $viewModel.type) + VStack(alignment: .leading, spacing: 16.0) { Text(VectorL10n.pollEditFormPollQuestionOrTopic) .font(theme.fonts.title3SB) @@ -58,7 +59,7 @@ struct PollEditForm: View { ForEach(0.. Void - - var body: some View { - VStack(alignment: .leading, spacing: 8.0) { - Text(VectorL10n.pollEditFormOptionNumber(index + 1)) - .font(theme.fonts.subheadline) - .foregroundColor(theme.colors.primaryContent) - - HStack(spacing: 16.0) { - TextField(VectorL10n.pollEditFormInputPlaceholder, text: $text, onEditingChanged: { edit in - self.focused = edit - }) - .textFieldStyle(BorderedInputFieldStyle(theme: _theme, isEditing: focused)) - Button { - onDelete() - } label: { - Image(uiImage:Asset.Images.pollDeleteOptionIcon.image) - } - .accessibilityIdentifier("Delete answer option") - } - } - } -} - // MARK: - Previews @available(iOS 14.0, *) diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditFormAnswerOptionView.swift b/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditFormAnswerOptionView.swift new file mode 100644 index 000000000..e3de14987 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditFormAnswerOptionView.swift @@ -0,0 +1,63 @@ +// +// 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 SwiftUI + +@available(iOS 14.0, *) +struct PollEditFormAnswerOptionView: View { + + @Environment(\.theme) private var theme: ThemeSwiftUI + + @State private var focused = false + + @Binding var text: String + + let index: Int + let onDelete: () -> Void + + var body: some View { + VStack(alignment: .leading, spacing: 8.0) { + Text(VectorL10n.pollEditFormOptionNumber(index + 1)) + .font(theme.fonts.subheadline) + .foregroundColor(theme.colors.primaryContent) + + HStack(spacing: 16.0) { + TextField(VectorL10n.pollEditFormInputPlaceholder, text: $text, onEditingChanged: { edit in + self.focused = edit + }) + .textFieldStyle(BorderedInputFieldStyle(theme: _theme, isEditing: focused)) + Button(action: onDelete) { + Image(uiImage:Asset.Images.pollDeleteOptionIcon.image) + } + .accessibilityIdentifier("Delete answer option") + } + } + } +} + +@available(iOS 14.0, *) +struct PollEditFormAnswerOptionView_Previews: PreviewProvider { + static var previews: some View { + VStack(spacing: 32.0) { + PollEditFormAnswerOptionView(text: Binding.constant(""), index: 0) { + + } + PollEditFormAnswerOptionView(text: Binding.constant("Test"), index: 5) { + + } + } + } +} diff --git a/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditFormTypePicker.swift b/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditFormTypePicker.swift new file mode 100644 index 000000000..590587d83 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/PollEditForm/View/PollEditFormTypePicker.swift @@ -0,0 +1,98 @@ +// +// 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 SwiftUI + +@available(iOS 14.0, *) +struct PollEditFormTypePicker: View { + @Environment(\.theme) private var theme: ThemeSwiftUI + + @Binding var selectedType: EditFormPollType + + var body: some View { + VStack(alignment: .leading, spacing: 16.0) { + Text(VectorL10n.pollEditFormPollType) + .font(theme.fonts.title3SB) + .foregroundColor(theme.colors.primaryContent) + PollEditFormTypeButton(type: .disclosed, selectedType: $selectedType) + PollEditFormTypeButton(type: .undisclosed, selectedType: $selectedType) + } + } +} + +@available(iOS 14.0, *) +private struct PollEditFormTypeButton: View { + @Environment(\.theme) private var theme: ThemeSwiftUI + + let type: EditFormPollType + @Binding var selectedType: EditFormPollType + + var body: some View { + Button { + selectedType = type + } label: { + HStack(alignment: .top, spacing: 8.0) { + + Image(uiImage: selectionImage) + + VStack(alignment: .leading, spacing: 2) { + Text(title) + .font(theme.fonts.body) + .foregroundColor(theme.colors.primaryContent) + Text(description) + .font(theme.fonts.footnote) + .foregroundColor(theme.colors.secondaryContent) + } + } + } + } + + private var title: String { + switch type { + case .disclosed: + return VectorL10n.pollEditFormPollTypeOpen + case .undisclosed: + return VectorL10n.pollEditFormPollTypeClosed + } + } + + private var description: String { + switch type { + case .disclosed: + return VectorL10n.pollEditFormPollTypeOpenDescription + case .undisclosed: + return VectorL10n.pollEditFormPollTypeClosedDescription + } + } + + private var selectionImage: UIImage { + if type == selectedType { + return Asset.Images.pollTypeCheckboxSelected.image + } else { + return Asset.Images.pollTypeCheckboxDefault.image + } + } +} + +@available(iOS 14.0, *) +struct PollEditFormTypePicker_Previews: PreviewProvider { + static var previews: some View { + VStack { + PollEditFormTypePicker(selectedType: Binding.constant(.disclosed)) + PollEditFormTypePicker(selectedType: Binding.constant(.undisclosed)) + } + } +} diff --git a/RiotSwiftUI/Modules/Room/PollTimeline/PollTimelineScreenState.swift b/RiotSwiftUI/Modules/Room/PollTimeline/PollTimelineScreenState.swift deleted file mode 100644 index b7752acf5..000000000 --- a/RiotSwiftUI/Modules/Room/PollTimeline/PollTimelineScreenState.swift +++ /dev/null @@ -1,47 +0,0 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/UserSuggestion UserSuggestion -// -// 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 SwiftUI - -@available(iOS 14.0, *) -enum MockPollTimelineScreenState: MockScreenState, CaseIterable { - case open - case closed - - var screenType: Any.Type { - MockPollTimelineScreenState.self - } - - var screenView: ([Any], AnyView) { - let answerOptions = [TimelineAnswerOption(id: "1", text: "First", count: 10, winner: false, selected: false), - TimelineAnswerOption(id: "2", text: "Second", count: 5, winner: false, selected: true), - TimelineAnswerOption(id: "3", text: "Third", count: 15, winner: true, selected: false)] - - let poll = TimelinePoll(question: "Question", - answerOptions: answerOptions, - closed: (self == .closed ? true : false), - totalAnswerCount: 20, - type: .disclosed, - maxAllowedSelections: 1) - - let viewModel = PollTimelineViewModel(timelinePoll: poll) - - return ([viewModel], AnyView(PollTimelineView(viewModel: viewModel.context))) - } -} diff --git a/RiotSwiftUI/Modules/Room/PollTimeline/View/PollTimelineAnswerOptionButton.swift b/RiotSwiftUI/Modules/Room/PollTimeline/View/PollTimelineAnswerOptionButton.swift deleted file mode 100644 index e5daad842..000000000 --- a/RiotSwiftUI/Modules/Room/PollTimeline/View/PollTimelineAnswerOptionButton.swift +++ /dev/null @@ -1,155 +0,0 @@ -// -// Copyright 2021 New Vector Ltd -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -import SwiftUI - -@available(iOS 14.0, *) -struct PollTimelineAnswerOptionButton: View { - - // MARK: - Properties - - // MARK: Private - - @Environment(\.theme) private var theme: ThemeSwiftUI - - let answerOption: TimelineAnswerOption - let pollClosed: Bool - let showResults: Bool - let totalAnswerCount: UInt - let action: () -> Void - - // MARK: Public - - var body: some View { - Button(action: action) { - let rect = RoundedRectangle(cornerRadius: 4.0) - answerOptionLabel - .padding(.horizontal, 8.0) - .padding(.top, 12.0) - .padding(.bottom, 4.0) - .clipShape(rect) - .overlay(rect.stroke(borderAccentColor, lineWidth: 1.0)) - .accentColor(progressViewAccentColor) - } - } - - var answerOptionLabel: some View { - VStack(alignment: .leading, spacing: 12.0) { - HStack(alignment: .top, spacing: 8.0) { - - if !pollClosed { - Image(uiImage: answerOption.selected ? Asset.Images.pollCheckboxSelected.image : Asset.Images.pollCheckboxDefault.image) - } - - Text(answerOption.text) - .font(theme.fonts.body) - .foregroundColor(theme.colors.primaryContent) - - if pollClosed && answerOption.winner { - Spacer() - Image(uiImage: Asset.Images.pollWinnerIcon.image) - } - } - - HStack { - ProgressView(value: Double(showResults ? answerOption.count : 0), - total: Double(totalAnswerCount)) - .progressViewStyle(LinearProgressViewStyle()) - .scaleEffect(x: 1.0, y: 1.2, anchor: .center) - .padding(.vertical, 8.0) - - if (showResults) { - Text(answerOption.count == 1 ? VectorL10n.pollTimelineOneVote : VectorL10n.pollTimelineVotesCount(Int(answerOption.count))) - .font(theme.fonts.footnote) - .foregroundColor(pollClosed && answerOption.winner ? theme.colors.accent : theme.colors.secondaryContent) - } - } - } - } - - var borderAccentColor: Color { - guard !pollClosed else { - return (answerOption.winner ? theme.colors.accent : theme.colors.quinaryContent) - } - - return answerOption.selected ? theme.colors.accent : theme.colors.quinaryContent - } - - var progressViewAccentColor: Color { - guard !pollClosed else { - return (answerOption.winner ? theme.colors.accent : theme.colors.quarterlyContent) - } - - return answerOption.selected ? theme.colors.accent : theme.colors.quarterlyContent - } -} - -@available(iOS 14.0, *) -struct PollTimelineAnswerOptionButton_Previews: PreviewProvider { - static let stateRenderer = MockPollTimelineScreenState.stateRenderer - static var previews: some View { - - Group { - VStack { - PollTimelineAnswerOptionButton(answerOption: TimelineAnswerOption(id: "", text: "Test", count: 5, winner: false, selected: false), - pollClosed: false, showResults: true, totalAnswerCount: 100, action: {}) - - PollTimelineAnswerOptionButton(answerOption: TimelineAnswerOption(id: "", text: "Test", count: 5, winner: false, selected: false), - pollClosed: false, showResults: false, totalAnswerCount: 100, action: {}) - - PollTimelineAnswerOptionButton(answerOption: TimelineAnswerOption(id: "", text: "Test", count: 8, winner: false, selected: true), - pollClosed: false, showResults: true, totalAnswerCount: 100, action: {}) - - PollTimelineAnswerOptionButton(answerOption: TimelineAnswerOption(id: "", text: "Test", count: 8, winner: false, selected: true), - pollClosed: false, showResults: false, totalAnswerCount: 100, action: {}) - - PollTimelineAnswerOptionButton(answerOption: TimelineAnswerOption(id: "", - text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", - count: 200, winner: false, selected: false), - pollClosed: false, showResults: true, totalAnswerCount: 1000, action: {}) - - PollTimelineAnswerOptionButton(answerOption: TimelineAnswerOption(id: "", - text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", - count: 200, winner: false, selected: false), - pollClosed: false, showResults: false, totalAnswerCount: 1000, action: {}) - } - - VStack { - PollTimelineAnswerOptionButton(answerOption: TimelineAnswerOption(id: "", text: "Test", count: 5, winner: false, selected: false), - pollClosed: true, showResults: true, totalAnswerCount: 100, action: {}) - - PollTimelineAnswerOptionButton(answerOption: TimelineAnswerOption(id: "", text: "Test", count: 5, winner: true, selected: false), - pollClosed: true, showResults: true, totalAnswerCount: 100, action: {}) - - PollTimelineAnswerOptionButton(answerOption: TimelineAnswerOption(id: "", text: "Test", count: 8, winner: false, selected: true), - pollClosed: true, showResults: true, totalAnswerCount: 100, action: {}) - - PollTimelineAnswerOptionButton(answerOption: TimelineAnswerOption(id: "", text: "Test", count: 8, winner: true, selected: true), - pollClosed: true, showResults: true, totalAnswerCount: 100, action: {}) - - PollTimelineAnswerOptionButton(answerOption: TimelineAnswerOption(id: "", - text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", - count: 200, winner: false, selected: false), - pollClosed: true, showResults: true, totalAnswerCount: 1000, action: {}) - - PollTimelineAnswerOptionButton(answerOption: TimelineAnswerOption(id: "", - text: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.", - count: 200, winner: true, selected: false), - pollClosed: true, showResults: true, totalAnswerCount: 1000, action: {}) - } - } - } -} diff --git a/RiotSwiftUI/Modules/Room/PollTimeline/Coordinator/PollTimelineCoordinator.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift similarity index 60% rename from RiotSwiftUI/Modules/Room/PollTimeline/Coordinator/PollTimelineCoordinator.swift rename to RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift index 58e782255..13d1f4233 100644 --- a/RiotSwiftUI/Modules/Room/PollTimeline/Coordinator/PollTimelineCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollCoordinator.swift @@ -1,43 +1,41 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/PollTimeline PollTimeline -/* - 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. - */ +// +// 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 SwiftUI import MatrixSDK import Combine -struct PollTimelineCoordinatorParameters { +struct TimelinePollCoordinatorParameters { let session: MXSession let room: MXRoom let pollStartEvent: MXEvent } @available(iOS 14.0, *) -final class PollTimelineCoordinator: Coordinator, Presentable, PollAggregatorDelegate { +final class TimelinePollCoordinator: Coordinator, Presentable, PollAggregatorDelegate { // MARK: - Properties // MARK: Private - private let parameters: PollTimelineCoordinatorParameters + private let parameters: TimelinePollCoordinatorParameters private let selectedAnswerIdentifiersSubject = PassthroughSubject<[String], Never>() private var pollAggregator: PollAggregator - private var pollTimelineViewModel: PollTimelineViewModel! + private var viewModel: TimelinePollViewModel! private var cancellables = Set() // MARK: Public @@ -48,14 +46,14 @@ final class PollTimelineCoordinator: Coordinator, Presentable, PollAggregatorDel // MARK: - Setup @available(iOS 14.0, *) - init(parameters: PollTimelineCoordinatorParameters) throws { + init(parameters: TimelinePollCoordinatorParameters) throws { self.parameters = parameters - try pollAggregator = PollAggregator(session: parameters.session, room: parameters.room, pollStartEvent: parameters.pollStartEvent) + try pollAggregator = PollAggregator(session: parameters.session, room: parameters.room, pollStartEventId: parameters.pollStartEvent.eventId) pollAggregator.delegate = self - pollTimelineViewModel = PollTimelineViewModel(timelinePoll: buildTimelinePollFrom(pollAggregator.poll)) - pollTimelineViewModel.callback = { [weak self] result in + viewModel = TimelinePollViewModel(timelinePollDetails: buildTimelinePollFrom(pollAggregator.poll)) + viewModel.callback = { [weak self] result in guard let self = self else { return } switch result { @@ -76,9 +74,9 @@ final class PollTimelineCoordinator: Coordinator, Presentable, PollAggregatorDel localEcho: nil, success: nil) { [weak self] error in guard let self = self else { return } - MXLog.error("[PollTimelineCoordinator]] Failed submitting response with error \(String(describing: error))") + MXLog.error("[TimelinePollCoordinator]] Failed submitting response with error \(String(describing: error))") - self.pollTimelineViewModel.dispatch(action: .showAnsweringFailure) + self.viewModel.dispatch(action: .showAnsweringFailure) } } .store(in: &cancellables) @@ -90,23 +88,28 @@ final class PollTimelineCoordinator: Coordinator, Presentable, PollAggregatorDel } func toPresentable() -> UIViewController { - return VectorHostingController(rootView: PollTimelineView(viewModel: pollTimelineViewModel.context)) + return VectorHostingController(rootView: TimelinePollView(viewModel: viewModel.context)) } func canEndPoll() -> Bool { return pollAggregator.poll.isClosed == false } + func canEditPoll() -> Bool { + return false // Intentionally disabled until platform parity. + // return pollAggregator.poll.isClosed == false && pollAggregator.poll.totalAnswerCount == 0 + } + func endPoll() { parameters.room.sendPollEnd(for: parameters.pollStartEvent, threadId: nil, localEcho: nil, success: nil) { [weak self] error in - self?.pollTimelineViewModel.dispatch(action: .showClosingFailure) + self?.viewModel.dispatch(action: .showClosingFailure) } } // MARK: - PollAggregatorDelegate func pollAggregatorDidUpdateData(_ aggregator: PollAggregator) { - pollTimelineViewModel.dispatch(action: .updateWithPoll(buildTimelinePollFrom(aggregator.poll))) + viewModel.dispatch(action: .updateWithPoll(buildTimelinePollFrom(aggregator.poll))) } func pollAggregatorDidStartLoading(_ aggregator: PollAggregator) { @@ -125,20 +128,28 @@ final class PollTimelineCoordinator: Coordinator, Presentable, PollAggregatorDel // PollProtocol is intentionally not available in the SwiftUI target as we don't want // to add the SDK as a dependency to it. We need to translate from one to the other on this level. - func buildTimelinePollFrom(_ poll: PollProtocol) -> TimelinePoll { + func buildTimelinePollFrom(_ poll: PollProtocol) -> TimelinePollDetails { let answerOptions = poll.answerOptions.map { pollAnswerOption in - TimelineAnswerOption(id: pollAnswerOption.id, + TimelinePollAnswerOption(id: pollAnswerOption.id, text: pollAnswerOption.text, count: pollAnswerOption.count, winner: pollAnswerOption.isWinner, selected: pollAnswerOption.isCurrentUserSelection) } - return TimelinePoll(question: poll.text, + return TimelinePollDetails(question: poll.text, answerOptions: answerOptions, closed: poll.isClosed, totalAnswerCount: poll.totalAnswerCount, - type: (poll.kind == .disclosed ? .disclosed : .undisclosed), - maxAllowedSelections: poll.maxAllowedSelections) + type: pollKindToTimelinePollType(poll.kind), + maxAllowedSelections: poll.maxAllowedSelections, + hasBeenEdited: poll.hasBeenEdited) + } + + private func pollKindToTimelinePollType(_ kind: PollKind) -> TimelinePollType { + let mapping = [PollKind.disclosed: TimelinePollType.disclosed, + PollKind.undisclosed: TimelinePollType.undisclosed] + + return mapping[kind] ?? .disclosed } } diff --git a/RiotSwiftUI/Modules/Room/PollTimeline/Coordinator/PollTimelineProvider.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollProvider.swift similarity index 78% rename from RiotSwiftUI/Modules/Room/PollTimeline/Coordinator/PollTimelineProvider.swift rename to RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollProvider.swift index d778cbedf..0fa488ebd 100644 --- a/RiotSwiftUI/Modules/Room/PollTimeline/Coordinator/PollTimelineProvider.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Coordinator/TimelinePollProvider.swift @@ -17,11 +17,11 @@ import Foundation @available(iOS 14, *) -class PollTimelineProvider { - static let shared = PollTimelineProvider() +class TimelinePollProvider { + static let shared = TimelinePollProvider() var session: MXSession? - var coordinatorsForEventIdentifiers = [String: PollTimelineCoordinator]() + var coordinatorsForEventIdentifiers = [String: TimelinePollCoordinator]() private init() { @@ -29,7 +29,7 @@ class PollTimelineProvider { /// Create or retrieve the poll timeline coordinator for this event and return /// a view to be displayed in the timeline - func buildPollTimelineViewForEvent(_ event: MXEvent) -> UIView? { + func buildTimelinePollViewForEvent(_ event: MXEvent) -> UIView? { guard let session = session, let room = session.room(withRoomId: event.roomId) else { return nil } @@ -38,8 +38,8 @@ class PollTimelineProvider { return coordinator.toPresentable().view } - let parameters = PollTimelineCoordinatorParameters(session: session, room: room, pollStartEvent: event) - guard let coordinator = try? PollTimelineCoordinator(parameters: parameters) else { + let parameters = TimelinePollCoordinatorParameters(session: session, room: room, pollStartEvent: event) + guard let coordinator = try? TimelinePollCoordinator(parameters: parameters) else { return nil } @@ -49,7 +49,7 @@ class PollTimelineProvider { } /// Retrieve the poll timeline coordinator for the given event or nil if it hasn't been created yet - func pollTimelineCoordinatorForEventIdentifier(_ eventIdentifier: String) -> PollTimelineCoordinator? { + func timelinePollCoordinatorForEventIdentifier(_ eventIdentifier: String) -> TimelinePollCoordinator? { return coordinatorsForEventIdentifiers[eventIdentifier] } } diff --git a/RiotSwiftUI/Modules/Room/PollTimeline/Test/UI/PollTimelineUITests.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Test/UI/TimelinePollUITests.swift similarity index 66% rename from RiotSwiftUI/Modules/Room/PollTimeline/Test/UI/PollTimelineUITests.swift rename to RiotSwiftUI/Modules/Room/TimelinePoll/Test/UI/TimelinePollUITests.swift index f36660e54..9c4efe9c1 100644 --- a/RiotSwiftUI/Modules/Room/PollTimeline/Test/UI/PollTimelineUITests.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Test/UI/TimelinePollUITests.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/PollTimeline PollTimeline // // Copyright 2021 New Vector Ltd // @@ -20,7 +18,7 @@ import XCTest import RiotSwiftUI @available(iOS 14.0, *) -class PollTimelineUITests: XCTestCase { +class TimelinePollUITests: XCTestCase { private var app: XCUIApplication! @@ -31,8 +29,8 @@ class PollTimelineUITests: XCTestCase { app.launch() } - func testOpenPoll() { - app.goToScreenWithIdentifier(MockPollTimelineScreenState.open.title) + func testOpenDisclosedPoll() { + app.goToScreenWithIdentifier(MockTimelinePollScreenState.openDisclosed.title) XCTAssert(app.staticTexts["Question"].exists) XCTAssert(app.staticTexts["20 votes cast"].exists) @@ -69,9 +67,48 @@ class PollTimelineUITests: XCTestCase { XCTAssertEqual(app.buttons["Third, 16 votes"].value as! String, "80%") } - func testClosedPoll() { - app.goToScreenWithIdentifier(MockPollTimelineScreenState.closed.title) + func testOpenUndisclosedPoll() { + app.goToScreenWithIdentifier(MockTimelinePollScreenState.openUndisclosed.title) + XCTAssert(app.staticTexts["Question"].exists) + XCTAssert(app.staticTexts["20 votes cast"].exists) + + XCTAssert(!app.buttons["First, 10 votes"].exists) + XCTAssert(app.buttons["First"].exists) + XCTAssertTrue((app.buttons["First"].value as! String).isEmpty) + + XCTAssert(!app.buttons["Second, 5 votes"].exists) + XCTAssert(app.buttons["Second"].exists) + XCTAssertTrue((app.buttons["Second"].value as! String).isEmpty) + + XCTAssert(!app.buttons["Third, 15 votes"].exists) + XCTAssert(app.buttons["Third"].exists) + XCTAssertTrue((app.buttons["Third"].value as! String).isEmpty) + + app.buttons["First"].tap() + + XCTAssert(app.buttons["First"].exists) + XCTAssert(app.buttons["Second"].exists) + XCTAssert(app.buttons["Third"].exists) + + app.buttons["Third"].tap() + + XCTAssert(app.buttons["First"].exists) + XCTAssert(app.buttons["Second"].exists) + XCTAssert(app.buttons["Third"].exists) + } + + func testClosedDisclosedPoll() { + app.goToScreenWithIdentifier(MockTimelinePollScreenState.closedDisclosed.title) + checkClosedPoll() + } + + func testClosedUndisclosedPoll() { + app.goToScreenWithIdentifier(MockTimelinePollScreenState.closedUndisclosed.title) + checkClosedPoll() + } + + private func checkClosedPoll() { XCTAssert(app.staticTexts["Question"].exists) XCTAssert(app.staticTexts["Final results based on 20 votes"].exists) diff --git a/RiotSwiftUI/Modules/Room/PollTimeline/Test/Unit/PollTimelineViewModelTests.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/Test/Unit/TimelinePollViewModelTests.swift similarity index 84% rename from RiotSwiftUI/Modules/Room/PollTimeline/Test/Unit/PollTimelineViewModelTests.swift rename to RiotSwiftUI/Modules/Room/TimelinePoll/Test/Unit/TimelinePollViewModelTests.swift index 9ad80e5b9..3de360418 100644 --- a/RiotSwiftUI/Modules/Room/PollTimeline/Test/Unit/PollTimelineViewModelTests.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/Test/Unit/TimelinePollViewModelTests.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/PollTimeline PollTimeline // // Copyright 2021 New Vector Ltd // @@ -22,24 +20,25 @@ import Combine @testable import RiotSwiftUI @available(iOS 14.0, *) -class PollTimelineViewModelTests: XCTestCase { - var viewModel: PollTimelineViewModel! - var context: PollTimelineViewModelType.Context! +class TimelinePollViewModelTests: XCTestCase { + var viewModel: TimelinePollViewModel! + var context: TimelinePollViewModelType.Context! var cancellables = Set() override func setUpWithError() throws { - let answerOptions = [TimelineAnswerOption(id: "1", text: "1", count: 1, winner: false, selected: false), - TimelineAnswerOption(id: "2", text: "2", count: 1, winner: false, selected: false), - TimelineAnswerOption(id: "3", text: "3", count: 1, winner: false, selected: false)] + let answerOptions = [TimelinePollAnswerOption(id: "1", text: "1", count: 1, winner: false, selected: false), + TimelinePollAnswerOption(id: "2", text: "2", count: 1, winner: false, selected: false), + TimelinePollAnswerOption(id: "3", text: "3", count: 1, winner: false, selected: false)] - let timelinePoll = TimelinePoll(question: "Question", - answerOptions: answerOptions, - closed: false, - totalAnswerCount: 3, - type: .disclosed, - maxAllowedSelections: 1) + let timelinePoll = TimelinePollDetails(question: "Question", + answerOptions: answerOptions, + closed: false, + totalAnswerCount: 3, + type: .disclosed, + maxAllowedSelections: 1, + hasBeenEdited: false) - viewModel = PollTimelineViewModel(timelinePoll: timelinePoll) + viewModel = TimelinePollViewModel(timelinePollDetails: timelinePoll) context = viewModel.context } diff --git a/RiotSwiftUI/Modules/Room/PollTimeline/PollTimelineModels.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift similarity index 56% rename from RiotSwiftUI/Modules/Room/PollTimeline/PollTimelineModels.swift rename to RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift index d01eaf864..da8abd7eb 100644 --- a/RiotSwiftUI/Modules/Room/PollTimeline/PollTimelineModels.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollModels.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/PollTimeline PollTimeline // // Copyright 2021 New Vector Ltd // @@ -19,20 +17,20 @@ import Foundation import SwiftUI -typealias PollTimelineViewModelCallback = ((PollTimelineViewModelResult) -> Void) +typealias TimelinePollViewModelCallback = ((TimelinePollViewModelResult) -> Void) -enum PollTimelineStateAction { - case viewAction(PollTimelineViewAction, PollTimelineViewModelCallback?) - case updateWithPoll(TimelinePoll) +enum TimelinePollStateAction { + case viewAction(TimelinePollViewAction, TimelinePollViewModelCallback?) + case updateWithPoll(TimelinePollDetails) case showAnsweringFailure case showClosingFailure } -enum PollTimelineViewAction { +enum TimelinePollViewAction { case selectAnswerOptionWithIdentifier(String) } -enum PollTimelineViewModelResult { +enum TimelinePollViewModelResult { case selectedAnswerOptionsWithIdentifiers([String]) } @@ -41,7 +39,7 @@ enum TimelinePollType { case undisclosed } -class TimelineAnswerOption: Identifiable { +class TimelinePollAnswerOption: Identifiable { var id: String var text: String var count: UInt @@ -57,35 +55,59 @@ class TimelineAnswerOption: Identifiable { } } -class TimelinePoll { +class TimelinePollDetails { var question: String - var answerOptions: [TimelineAnswerOption] + var answerOptions: [TimelinePollAnswerOption] var closed: Bool var totalAnswerCount: UInt var type: TimelinePollType var maxAllowedSelections: UInt + var hasBeenEdited: Bool = true - init(question: String, answerOptions: [TimelineAnswerOption], closed: Bool, totalAnswerCount: UInt, type: TimelinePollType, maxAllowedSelections: UInt) { + init(question: String, answerOptions: [TimelinePollAnswerOption], + closed: Bool, + totalAnswerCount: UInt, + type: TimelinePollType, + maxAllowedSelections: UInt, + hasBeenEdited: Bool) { self.question = question self.answerOptions = answerOptions self.closed = closed self.totalAnswerCount = totalAnswerCount self.type = type self.maxAllowedSelections = maxAllowedSelections + self.hasBeenEdited = hasBeenEdited } var hasCurrentUserVoted: Bool { answerOptions.filter { $0.selected == true}.count > 0 } + + var shouldDiscloseResults: Bool { + if closed { + return totalAnswerCount > 0 + } else { + return type == .disclosed && totalAnswerCount > 0 && hasCurrentUserVoted + } + } } -struct PollTimelineViewState: BindableState { - var poll: TimelinePoll - var bindings: PollTimelineViewStateBindings +struct TimelinePollViewState: BindableState { + var poll: TimelinePollDetails + var bindings: TimelinePollViewStateBindings } -struct PollTimelineViewStateBindings { - var showsAnsweringFailureAlert: Bool = false - var showsClosingFailureAlert: Bool = false +struct TimelinePollViewStateBindings { + var alertInfo: TimelinePollErrorAlertInfo? } +struct TimelinePollErrorAlertInfo: Identifiable { + enum AlertType { + case failedClosingPoll + case failedSubmittingAnswer + } + + let id: AlertType + let title: String + let subtitle: String +} diff --git a/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift new file mode 100644 index 000000000..3fe93f8b8 --- /dev/null +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollScreenState.swift @@ -0,0 +1,48 @@ +// +// 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 SwiftUI + +@available(iOS 14.0, *) +enum MockTimelinePollScreenState: MockScreenState, CaseIterable { + case openDisclosed + case closedDisclosed + case openUndisclosed + case closedUndisclosed + + var screenType: Any.Type { + TimelinePollDetails.self + } + + var screenView: ([Any], AnyView) { + let answerOptions = [TimelinePollAnswerOption(id: "1", text: "First", count: 10, winner: false, selected: false), + TimelinePollAnswerOption(id: "2", text: "Second", count: 5, winner: false, selected: true), + TimelinePollAnswerOption(id: "3", text: "Third", count: 15, winner: true, selected: false)] + + let poll = TimelinePollDetails(question: "Question", + answerOptions: answerOptions, + closed: (self == .closedDisclosed || self == .closedUndisclosed ? true : false), + totalAnswerCount: 20, + type: (self == .closedDisclosed || self == .openDisclosed ? .disclosed : .undisclosed), + maxAllowedSelections: 1, + hasBeenEdited: false) + + let viewModel = TimelinePollViewModel(timelinePollDetails: poll) + + return ([viewModel], AnyView(TimelinePollView(viewModel: viewModel.context))) + } +} diff --git a/RiotSwiftUI/Modules/Room/PollTimeline/PollTimelineViewModel.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModel.swift similarity index 68% rename from RiotSwiftUI/Modules/Room/PollTimeline/PollTimelineViewModel.swift rename to RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModel.swift index eb802509b..f28c7185c 100644 --- a/RiotSwiftUI/Modules/Room/PollTimeline/PollTimelineViewModel.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/TimelinePollViewModel.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/PollTimeline PollTimeline // // Copyright 2021 New Vector Ltd // @@ -20,11 +18,11 @@ import SwiftUI import Combine @available(iOS 14, *) -typealias PollTimelineViewModelType = StateStoreViewModel +typealias TimelinePollViewModelType = StateStoreViewModel @available(iOS 14, *) -class PollTimelineViewModel: PollTimelineViewModelType { +class TimelinePollViewModel: TimelinePollViewModelType { // MARK: - Properties @@ -32,24 +30,24 @@ class PollTimelineViewModel: PollTimelineViewModelType { // MARK: Public - var callback: PollTimelineViewModelCallback? + var callback: TimelinePollViewModelCallback? // MARK: - Setup - init(timelinePoll: TimelinePoll) { - super.init(initialViewState: PollTimelineViewState(poll: timelinePoll, bindings: PollTimelineViewStateBindings())) + init(timelinePollDetails: TimelinePollDetails) { + super.init(initialViewState: TimelinePollViewState(poll: timelinePollDetails, bindings: TimelinePollViewStateBindings())) } // MARK: - Public - override func process(viewAction: PollTimelineViewAction) { + override func process(viewAction: TimelinePollViewAction) { switch viewAction { case .selectAnswerOptionWithIdentifier(_): dispatch(action: .viewAction(viewAction, callback)) } } - override class func reducer(state: inout PollTimelineViewState, action: PollTimelineStateAction) { + override class func reducer(state: inout TimelinePollViewState, action: TimelinePollStateAction) { switch action { case .viewAction(let viewAction, let callback): switch viewAction { @@ -69,15 +67,19 @@ class PollTimelineViewModel: PollTimelineViewModelType { case .updateWithPoll(let poll): state.poll = poll case .showAnsweringFailure: - state.bindings.showsAnsweringFailureAlert = true + state.bindings.alertInfo = TimelinePollErrorAlertInfo(id: .failedSubmittingAnswer, + title: VectorL10n.pollTimelineVoteNotRegisteredTitle, + subtitle: VectorL10n.pollTimelineVoteNotRegisteredSubtitle) case .showClosingFailure: - state.bindings.showsClosingFailureAlert = true + state.bindings.alertInfo = TimelinePollErrorAlertInfo(id: .failedClosingPoll, + title: VectorL10n.pollTimelineNotClosedTitle, + subtitle: VectorL10n.pollTimelineNotClosedSubtitle) } } // MARK: - Private - static func updateSingleSelectPollLocalState(_ state: inout PollTimelineViewState, selectedAnswerIdentifier: String, callback: PollTimelineViewModelCallback?) { + static func updateSingleSelectPollLocalState(_ state: inout TimelinePollViewState, selectedAnswerIdentifier: String, callback: TimelinePollViewModelCallback?) { for answerOption in state.poll.answerOptions { if answerOption.selected { answerOption.selected = false @@ -98,7 +100,7 @@ class PollTimelineViewModel: PollTimelineViewModelType { informCoordinatorOfSelectionUpdate(state: state, callback: callback) } - static func updateMultiSelectPollLocalState(_ state: inout PollTimelineViewState, selectedAnswerIdentifier: String, callback: PollTimelineViewModelCallback?) { + static 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 @@ -122,7 +124,7 @@ class PollTimelineViewModel: PollTimelineViewModelType { informCoordinatorOfSelectionUpdate(state: state, callback: callback) } - static func informCoordinatorOfSelectionUpdate(state: PollTimelineViewState, callback: PollTimelineViewModelCallback?) { + static 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/View/TimelinePollAnswerOptionButton.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift new file mode 100644 index 000000000..7cd02911d --- /dev/null +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollAnswerOptionButton.swift @@ -0,0 +1,157 @@ +// +// 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 SwiftUI + +@available(iOS 14.0, *) +struct TimelinePollAnswerOptionButton: View { + + // MARK: - Properties + + // MARK: Private + + @Environment(\.theme) private var theme: ThemeSwiftUI + + let poll: TimelinePollDetails + let answerOption: TimelinePollAnswerOption + let action: () -> Void + + // MARK: Public + + var body: some View { + Button(action: action) { + let rect = RoundedRectangle(cornerRadius: 4.0) + answerOptionLabel + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, 8.0) + .padding(.top, 12.0) + .padding(.bottom, 12.0) + .clipShape(rect) + .overlay(rect.stroke(borderAccentColor, lineWidth: 1.0)) + .accentColor(progressViewAccentColor) + } + } + + var answerOptionLabel: some View { + VStack(alignment: .leading, spacing: 12.0) { + HStack(alignment: .top, spacing: 8.0) { + + if !poll.closed { + Image(uiImage: answerOption.selected ? Asset.Images.pollCheckboxSelected.image : Asset.Images.pollCheckboxDefault.image) + } + + Text(answerOption.text) + .font(theme.fonts.body) + .foregroundColor(theme.colors.primaryContent) + + if poll.closed && answerOption.winner { + Spacer() + Image(uiImage: Asset.Images.pollWinnerIcon.image) + } + } + + if poll.type == .disclosed || poll.closed { + HStack { + ProgressView(value: Double(poll.shouldDiscloseResults ? answerOption.count : 0), + total: Double(poll.totalAnswerCount)) + .progressViewStyle(LinearProgressViewStyle()) + .scaleEffect(x: 1.0, y: 1.2, anchor: .center) + + if (poll.shouldDiscloseResults) { + Text(answerOption.count == 1 ? VectorL10n.pollTimelineOneVote : VectorL10n.pollTimelineVotesCount(Int(answerOption.count))) + .font(theme.fonts.footnote) + .foregroundColor(poll.closed && answerOption.winner ? theme.colors.accent : theme.colors.secondaryContent) + } + } + } + } + } + + var borderAccentColor: Color { + guard !poll.closed else { + return (answerOption.winner ? theme.colors.accent : theme.colors.quinaryContent) + } + + return answerOption.selected ? theme.colors.accent : theme.colors.quinaryContent + } + + var progressViewAccentColor: Color { + guard !poll.closed else { + return (answerOption.winner ? theme.colors.accent : theme.colors.quarterlyContent) + } + + return answerOption.selected ? theme.colors.accent : theme.colors.quarterlyContent + } +} + +@available(iOS 14.0, *) +struct TimelinePollAnswerOptionButton_Previews: PreviewProvider { + static let stateRenderer = MockTimelinePollScreenState.stateRenderer + + static var previews: some View { + Group { + let pollTypes: [TimelinePollType] = [.disclosed, .undisclosed] + + ForEach(pollTypes, id: \.self) { type in + VStack { + TimelinePollAnswerOptionButton(poll: buildPoll(closed: false, type: type), + answerOption: buildAnswerOption(selected: false), + action: {}) + + TimelinePollAnswerOptionButton(poll: buildPoll(closed: false, type: type), + answerOption: buildAnswerOption(selected: true), + action: {}) + + TimelinePollAnswerOptionButton(poll: buildPoll(closed: true, type: type), + answerOption: buildAnswerOption(selected: false, winner: false), + action: {}) + + TimelinePollAnswerOptionButton(poll: buildPoll(closed: true, type: type), + answerOption: buildAnswerOption(selected: false, winner: true), + action: {}) + + TimelinePollAnswerOptionButton(poll: buildPoll(closed: true, type: type), + answerOption: buildAnswerOption(selected: true, winner: false), + action: {}) + + TimelinePollAnswerOptionButton(poll: buildPoll(closed: true, type: type), + answerOption: buildAnswerOption(selected: true, winner: true), + action: {}) + + let longText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat." + + TimelinePollAnswerOptionButton(poll: buildPoll(closed: true, type: type), + answerOption: buildAnswerOption(text: longText, selected: true, winner: true), + action: {}) + } + } + } + } + + static func buildPoll(closed: Bool, type: TimelinePollType) -> TimelinePollDetails { + TimelinePollDetails(question: "", + answerOptions: [], + closed: closed, + totalAnswerCount: 100, + type: type, + maxAllowedSelections: 1, + hasBeenEdited: false) + } + + static func buildAnswerOption(text: String = "Test", selected: Bool, winner: Bool = false) -> TimelinePollAnswerOption { + TimelinePollAnswerOption(id: "1", text: text, count: 5, winner: winner, selected: selected) + } +} diff --git a/RiotSwiftUI/Modules/Room/PollTimeline/View/PollTimelineView.swift b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift similarity index 60% rename from RiotSwiftUI/Modules/Room/PollTimeline/View/PollTimelineView.swift rename to RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift index 70efcc067..89efbeb17 100644 --- a/RiotSwiftUI/Modules/Room/PollTimeline/View/PollTimelineView.swift +++ b/RiotSwiftUI/Modules/Room/TimelinePoll/View/TimelinePollView.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/PollEditForm PollEditForm // // Copyright 2021 New Vector Ltd // @@ -19,7 +17,7 @@ import SwiftUI @available(iOS 14.0, *) -struct PollTimelineView: View { +struct TimelinePollView: View { // MARK: - Properties @@ -29,29 +27,26 @@ struct PollTimelineView: View { // MARK: Public - @ObservedObject var viewModel: PollTimelineViewModel.Context + @ObservedObject var viewModel: TimelinePollViewModel.Context var body: some View { let poll = viewModel.viewState.poll VStack(alignment: .leading, spacing: 16.0) { + Text(poll.question) .font(theme.fonts.bodySB) + .foregroundColor(theme.colors.primaryContent) + + Text(editedText) + .font(theme.fonts.footnote) + .foregroundColor(theme.colors.secondaryContent) VStack(spacing: 24.0) { ForEach(poll.answerOptions) { answerOption in - PollTimelineAnswerOptionButton(answerOption: answerOption, - pollClosed: poll.closed, - showResults: shouldDiscloseResults, - totalAnswerCount: poll.totalAnswerCount) { + TimelinePollAnswerOptionButton(poll: poll, answerOption: answerOption) { viewModel.send(viewAction: .selectAnswerOptionWithIdentifier(answerOption.id)) } } - .alert(isPresented: $viewModel.showsClosingFailureAlert) { - Alert(title: Text(VectorL10n.pollTimelineNotClosedTitle), - message: Text(VectorL10n.pollTimelineNotClosedSubtitle), - dismissButton: .default(Text(VectorL10n.ok))) - } } .disabled(poll.closed) .fixedSize(horizontal: false, vertical: true) @@ -59,14 +54,14 @@ struct PollTimelineView: View { Text(totalVotesString) .font(theme.fonts.footnote) .foregroundColor(theme.colors.tertiaryContent) - .alert(isPresented: $viewModel.showsAnsweringFailureAlert) { - Alert(title: Text(VectorL10n.pollTimelineVoteNotRegisteredTitle), - message: Text(VectorL10n.pollTimelineVoteNotRegisteredSubtitle), - dismissButton: .default(Text(VectorL10n.ok))) - } } .padding([.horizontal, .top], 2.0) .padding([.bottom]) + .alert(item: $viewModel.alertInfo) { info in + Alert(title: Text(info.title), + message: Text(info.subtitle), + dismissButton: .default(Text(VectorL10n.ok))) + } } private var totalVotesString: String { @@ -84,32 +79,26 @@ struct PollTimelineView: View { case 0: return VectorL10n.pollTimelineTotalNoVotes case 1: - return (poll.hasCurrentUserVoted ? + return (poll.hasCurrentUserVoted || poll.type == .undisclosed ? VectorL10n.pollTimelineTotalOneVote : VectorL10n.pollTimelineTotalOneVoteNotVoted) default: - return (poll.hasCurrentUserVoted ? + return (poll.hasCurrentUserVoted || poll.type == .undisclosed ? VectorL10n.pollTimelineTotalVotes(Int(poll.totalAnswerCount)) : VectorL10n.pollTimelineTotalVotesNotVoted(Int(poll.totalAnswerCount))) } } - private var shouldDiscloseResults: Bool { - let poll = viewModel.viewState.poll - - if poll.closed { - return poll.totalAnswerCount > 0 - } else { - return poll.type == .disclosed && poll.totalAnswerCount > 0 && poll.hasCurrentUserVoted - } + private var editedText: String { + viewModel.viewState.poll.hasBeenEdited ? " \(VectorL10n.eventFormatterMessageEditedMention)" : "" } } // MARK: - Previews @available(iOS 14.0, *) -struct PollTimelineView_Previews: PreviewProvider { - static let stateRenderer = MockPollTimelineScreenState.stateRenderer +struct TimelinePollView_Previews: PreviewProvider { + static let stateRenderer = MockTimelinePollScreenState.stateRenderer static var previews: some View { stateRenderer.screenGroup() } diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift index d960a9796..4163d0668 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinator.swift @@ -1,20 +1,18 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/UserSuggestion UserSuggestion -/* - 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. - */ +// +// Copyright 2021 New Vector Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// import Foundation import UIKit diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorParameters.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorParameters.swift index dbad53c94..bd9d54e8b 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorParameters.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Coordinator/UserSuggestionCoordinatorParameters.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/UserSuggestion UserSuggestion // // Copyright 2021 New Vector Ltd // diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionStateAction.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionStateAction.swift index 153848943..f21630348 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionStateAction.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionStateAction.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/UserSuggestion UserSuggestion // // Copyright 2021 New Vector Ltd // diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewAction.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewAction.swift index 6352ed5ae..d08fa62ec 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewAction.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewAction.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/UserSuggestion UserSuggestion // // Copyright 2021 New Vector Ltd // diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewModelResult.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewModelResult.swift index 0336e5be0..b15a983b6 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewModelResult.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewModelResult.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/UserSuggestion UserSuggestion // // Copyright 2021 New Vector Ltd // diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewState.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewState.swift index b47e436c4..0b992b6d1 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewState.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Model/UserSuggestionViewState.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/UserSuggestion UserSuggestion // // Copyright 2021 New Vector Ltd // diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/Mock/MockUserSuggestionScreenState.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/Mock/MockUserSuggestionScreenState.swift index 7d4180201..16941ab76 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/Mock/MockUserSuggestionScreenState.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/Mock/MockUserSuggestionScreenState.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/UserSuggestion UserSuggestion // // Copyright 2021 New Vector Ltd // @@ -26,7 +24,7 @@ enum MockUserSuggestionScreenState: MockScreenState, CaseIterable { static private var members: [RoomMembersProviderMember]! var screenType: Any.Type { - MockUserSuggestionScreenState.self + UserSuggestionList.self } var screenView: ([Any], AnyView) { diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift index 1491aa579..a2a59ec86 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionService.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/UserSuggestion UserSuggestion // // Copyright 2021 New Vector Ltd // diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift index 42faeb6c7..44009d0c8 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Service/UserSuggestionServiceProtocol.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/UserSuggestion UserSuggestion // // Copyright 2021 New Vector Ltd // diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Test/UI/UserSuggestionUITests.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Test/UI/UserSuggestionUITests.swift index c0fa3c926..af864f6a7 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Test/UI/UserSuggestionUITests.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Test/UI/UserSuggestionUITests.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/UserSuggestion UserSuggestion // // Copyright 2021 New Vector Ltd // diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionServiceTests.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionServiceTests.swift index fce852059..a8eaf6e45 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionServiceTests.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/Test/Unit/UserSuggestionServiceTests.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/UserSuggestion UserSuggestion // // Copyright 2021 New Vector Ltd // diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift index a8c400a1f..4063e75a9 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionList.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/UserSuggestion UserSuggestion // // Copyright 2021 New Vector Ltd // diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift index dcfa53fa8..dd9068fdc 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/View/UserSuggestionListItem.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/UserSuggestion UserSuggestion // // Copyright 2021 New Vector Ltd // diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/ViewModel/UserSuggestionViewModel.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/ViewModel/UserSuggestionViewModel.swift index 3d7dca491..62ba522e4 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/ViewModel/UserSuggestionViewModel.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/ViewModel/UserSuggestionViewModel.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/UserSuggestion UserSuggestion // // Copyright 2021 New Vector Ltd // diff --git a/RiotSwiftUI/Modules/Room/UserSuggestion/ViewModel/UserSuggestionViewModelProtocol.swift b/RiotSwiftUI/Modules/Room/UserSuggestion/ViewModel/UserSuggestionViewModelProtocol.swift index 10207210c..169b89735 100644 --- a/RiotSwiftUI/Modules/Room/UserSuggestion/ViewModel/UserSuggestionViewModelProtocol.swift +++ b/RiotSwiftUI/Modules/Room/UserSuggestion/ViewModel/UserSuggestionViewModelProtocol.swift @@ -1,5 +1,3 @@ -// File created from SimpleUserProfileExample -// $ createScreen.sh Room/UserSuggestion UserSuggestion // // Copyright 2021 New Vector Ltd // diff --git a/RiotSwiftUI/Modules/Settings/Notifications/Coordinator/NotificationSettingsCoordinator.swift b/RiotSwiftUI/Modules/Settings/Notifications/Coordinator/NotificationSettingsCoordinator.swift index 06b8a6518..eeb9e0b00 100644 --- a/RiotSwiftUI/Modules/Settings/Notifications/Coordinator/NotificationSettingsCoordinator.swift +++ b/RiotSwiftUI/Modules/Settings/Notifications/Coordinator/NotificationSettingsCoordinator.swift @@ -1,20 +1,18 @@ -// File created from ScreenTemplate -// $ createScreen.sh Settings/Notifications NotificationSettings -/* - 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. - */ +// +// 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 SwiftUI diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/Coordinator/TemplateRoomsCoordinator.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/Coordinator/TemplateRoomsCoordinator.swift index d7af25881..be453b948 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/Coordinator/TemplateRoomsCoordinator.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/Coordinator/TemplateRoomsCoordinator.swift @@ -1,20 +1,18 @@ -// File created from FlowTemplate -// $ createRootCoordinator.sh TemplateRoomsCoordinator TemplateRooms -/* - 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. - */ +// +// 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 diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/Coordinator/TemplateRoomsCoordinatorParameters.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/Coordinator/TemplateRoomsCoordinatorParameters.swift index 8454b3ee2..dd9cd4295 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/Coordinator/TemplateRoomsCoordinatorParameters.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/Coordinator/TemplateRoomsCoordinatorParameters.swift @@ -1,20 +1,18 @@ -// File created from FlowTemplate -// $ createRootCoordinator.sh TemplateRoomsCoordinator TemplateRooms -/* - 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. - */ +// +// 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 diff --git a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/View/TemplateRoomListRow.swift b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/View/TemplateRoomListRow.swift index d417c315b..a61fef6c3 100644 --- a/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/View/TemplateRoomListRow.swift +++ b/RiotSwiftUI/Modules/Template/TemplateAdvancedRoomsExample/TemplateRoomList/View/TemplateRoomListRow.swift @@ -35,7 +35,7 @@ struct TemplateRoomListRow: View { AvatarImage(avatarData: avatar, size: .medium) Text(displayName ?? "") .foregroundColor(theme.colors.primaryContent) - .accessibility(identifier: "roomNameText") + .accessibility(identifier: "roomNameText") Spacer() } //add to a style