mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-17 15:09:31 +02:00
Merge branch 'develop' into ismail/5068_start_thread
This commit is contained in:
@@ -375,6 +375,6 @@ final class BuildSettings: NSObject {
|
||||
return false
|
||||
}
|
||||
|
||||
return false
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
23
Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_default.imageset/Contents.json
vendored
Normal file
23
Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_default.imageset/Contents.json
vendored
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 615 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
23
Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_selected.imageset/Contents.json
vendored
Normal file
23
Riot/Assets/Images.xcassets/Room/Polls/poll_type_checkbox_selected.imageset/Contents.json
vendored
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 729 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.0 KiB |
@@ -1839,6 +1839,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";
|
||||
@@ -1855,6 +1857,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";
|
||||
|
||||
@@ -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 urlPreviewClose = ImageAsset(name: "url_preview_close")
|
||||
|
||||
@@ -2539,6 +2539,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 +2572,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")
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -2089,8 +2089,8 @@ typedef NS_ENUM (NSUInteger, MXKRoomDataSourceError) {
|
||||
[self removeEventWithEventId:eventId];
|
||||
|
||||
if (event.isVoiceMessage) {
|
||||
NSNumber *duration = event.content[kMXMessageContentKeyExtensibleAudio][kMXMessageContentKeyExtensibleAudioDuration];
|
||||
NSArray<NSNumber *> *samples = event.content[kMXMessageContentKeyExtensibleAudio][kMXMessageContentKeyExtensibleAudioWaveform];
|
||||
NSNumber *duration = event.content[kMXMessageContentKeyExtensibleAudioMSC1767][kMXMessageContentKeyExtensibleAudioDuration];
|
||||
NSArray<NSNumber *> *samples = event.content[kMXMessageContentKeyExtensibleAudioMSC1767][kMXMessageContentKeyExtensibleAudioWaveform];
|
||||
|
||||
[self sendVoiceMessage:localFileURL mimeType:mimetype duration:duration.doubleValue samples:samples success:success failure:failure];
|
||||
} else {
|
||||
|
||||
@@ -1577,6 +1577,11 @@ static NSString *const kHTMLATagRegexPattern = @"<a href=\"(.*?)\">([^<]*)</a>";
|
||||
}
|
||||
case MXEventTypePollStart:
|
||||
{
|
||||
if (event.isEditEvent)
|
||||
{
|
||||
return nil;
|
||||
}
|
||||
|
||||
displayText = [MXEventContentPollStart modelFromJSON:event.content].question;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -279,6 +279,10 @@ NSString *const URLPreviewDidUpdateNotification = @"URLPreviewDidUpdateNotificat
|
||||
|
||||
if (self.tag == RoomBubbleCellDataTagPoll)
|
||||
{
|
||||
if (self.events.lastObject.isEditEvent) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2090,7 +2090,7 @@ const NSTimeInterval kResizeComposerAnimationDuration = .05;
|
||||
[self roomInputToolbarViewDidTapFileUpload];
|
||||
}]];
|
||||
}
|
||||
if (RiotSettings.shared.roomScreenAllowPollsAction)
|
||||
if (BuildSettings.pollsEnabled)
|
||||
{
|
||||
[actionItems addObject:[[RoomActionItem alloc] initWithImage:[UIImage imageNamed:@"action_poll"] andAction:^{
|
||||
MXStrongifyAndReturnIfNil(self);
|
||||
@@ -6257,16 +6257,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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -164,7 +164,6 @@ typedef NS_ENUM(NSUInteger, ABOUT)
|
||||
typedef NS_ENUM(NSUInteger, LABS_ENABLE)
|
||||
{
|
||||
LABS_ENABLE_RINGING_FOR_GROUP_CALLS_INDEX = 0,
|
||||
LABS_ENABLE_POLLS,
|
||||
LABS_ENABLE_THREADS_INDEX
|
||||
};
|
||||
|
||||
@@ -580,7 +579,6 @@ TableViewSectionsDelegate>
|
||||
{
|
||||
Section *sectionLabs = [Section sectionWithTag:SECTION_TAG_LABS];
|
||||
[sectionLabs addRowWithTag:LABS_ENABLE_RINGING_FOR_GROUP_CALLS_INDEX];
|
||||
[sectionLabs addRowWithTag:LABS_ENABLE_POLLS];
|
||||
[sectionLabs addRowWithTag:LABS_ENABLE_THREADS_INDEX];
|
||||
sectionLabs.headerTitle = [VectorL10n settingsLabs];
|
||||
if (sectionLabs.hasAnyRows)
|
||||
@@ -2465,18 +2463,6 @@ TableViewSectionsDelegate>
|
||||
|
||||
cell = labelAndSwitchCell;
|
||||
}
|
||||
else if (row == LABS_ENABLE_POLLS && BuildSettings.pollsEnabled)
|
||||
{
|
||||
MXKTableViewCellWithLabelAndSwitch *labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath];
|
||||
|
||||
labelAndSwitchCell.mxkLabel.text = [VectorL10n settingsLabsEnabledPolls];
|
||||
labelAndSwitchCell.mxkSwitch.on = RiotSettings.shared.roomScreenAllowPollsAction;
|
||||
labelAndSwitchCell.mxkSwitch.onTintColor = ThemeService.shared.theme.tintColor;
|
||||
|
||||
[labelAndSwitchCell.mxkSwitch addTarget:self action:@selector(toggleEnablePolls:) forControlEvents:UIControlEventValueChanged];
|
||||
|
||||
cell = labelAndSwitchCell;
|
||||
}
|
||||
else if (row == LABS_ENABLE_THREADS_INDEX)
|
||||
{
|
||||
MXKTableViewCellWithLabelAndSwitch *labelAndSwitchCell = [self getLabelAndSwitchCell:tableView forIndexPath:indexPath];
|
||||
@@ -3221,11 +3207,6 @@ TableViewSectionsDelegate>
|
||||
RiotSettings.shared.enableRingingForGroupCalls = sender.isOn;
|
||||
}
|
||||
|
||||
- (void)toggleEnablePolls:(UISwitch *)sender
|
||||
{
|
||||
RiotSettings.shared.roomScreenAllowPollsAction = sender.isOn;
|
||||
}
|
||||
|
||||
- (void)toggleEnableThreads:(UISwitch *)sender
|
||||
{
|
||||
RiotSettings.shared.enableThreads = sender.isOn;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh AnalyticsPrompt AnalyticsPrompt
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh AnalyticsPrompt AnalyticsPrompt
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh AnalyticsPrompt AnalyticsPrompt
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh AnalyticsPrompt AnalyticsPrompt
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh AnalyticsPrompt AnalyticsPrompt
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
|
||||
@@ -24,7 +24,7 @@ enum MockAppScreens {
|
||||
MockAnalyticsPromptScreenState.self,
|
||||
MockUserSuggestionScreenState.self,
|
||||
MockPollEditFormScreenState.self,
|
||||
MockPollTimelineScreenState.self,
|
||||
MockTimelinePollScreenState.self,
|
||||
MockTemplateUserProfileScreenState.self,
|
||||
MockTemplateRoomListScreenState.self,
|
||||
MockTemplateRoomChatScreenState.self
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -25,7 +25,7 @@ enum MockLocationSharingScreenState: MockScreenState, CaseIterable {
|
||||
case displayExistingLocation
|
||||
|
||||
var screenType: Any.Type {
|
||||
MockLocationSharingScreenState.self
|
||||
LocationSharingView.self
|
||||
}
|
||||
|
||||
var screenView: ([Any], AnyView) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/PollEditForm PollEditForm
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
|
||||
@@ -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<AnyCancellable>()
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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..<viewModel.answerOptions.count, id: \.self) { index in
|
||||
SafeBindingCollectionEnumerator($viewModel.answerOptions, index: index) { binding in
|
||||
AnswerOptionGroup(text: binding.text, index: index) {
|
||||
PollEditFormAnswerOptionView(text: binding.text, index: index) {
|
||||
withAnimation(.easeInOut(duration: 0.2)) {
|
||||
viewModel.send(viewAction: .deleteAnswerOption(viewModel.answerOptions[index]))
|
||||
}
|
||||
@@ -76,17 +77,20 @@ struct PollEditForm: View {
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(VectorL10n.pollEditFormCreatePoll) {
|
||||
viewModel.send(viewAction: .create)
|
||||
if viewModel.viewState.mode == .creation {
|
||||
Button(VectorL10n.pollEditFormCreatePoll) {
|
||||
viewModel.send(viewAction: .create)
|
||||
}
|
||||
.buttonStyle(PrimaryActionButtonStyle())
|
||||
.disabled(!viewModel.viewState.confirmationButtonEnabled)
|
||||
}
|
||||
.buttonStyle(PrimaryActionButtonStyle())
|
||||
.disabled(!viewModel.viewState.confirmationButtonEnabled)
|
||||
}
|
||||
.padding()
|
||||
.padding(.vertical, 24.0)
|
||||
.padding(.horizontal, 16.0)
|
||||
.activityIndicator(show: viewModel.viewState.showLoadingIndicator)
|
||||
.alert(isPresented: $viewModel.showsFailureAlert) {
|
||||
Alert(title: Text(VectorL10n.pollEditFormPostFailureTitle),
|
||||
message: Text(VectorL10n.pollEditFormPostFailureSubtitle),
|
||||
.alert(item: $viewModel.alertInfo) { info in
|
||||
Alert(title: Text(info.title),
|
||||
message: Text(info.subtitle),
|
||||
dismissButton: .default(Text(VectorL10n.ok)))
|
||||
}
|
||||
.frame(minHeight: proxy.size.height) // Make the VStack fill the ScrollView's parent
|
||||
@@ -101,6 +105,15 @@ struct PollEditForm: View {
|
||||
.font(.headline)
|
||||
.foregroundColor(theme.colors.primaryContent)
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
if viewModel.viewState.mode == .editing {
|
||||
Button(VectorL10n.save, action: {
|
||||
viewModel.send(viewAction: .update)
|
||||
})
|
||||
.disabled(!viewModel.viewState.confirmationButtonEnabled)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
@@ -111,40 +124,6 @@ struct PollEditForm: View {
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
private struct AnswerOptionGroup: 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 {
|
||||
onDelete()
|
||||
} label: {
|
||||
Image(uiImage:Asset.Images.pollDeleteOptionIcon.image)
|
||||
}
|
||||
.accessibilityIdentifier("Delete answer option")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)))
|
||||
}
|
||||
}
|
||||
@@ -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: {})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<AnyCancellable>()
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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<AnyCancellable>()
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)))
|
||||
}
|
||||
}
|
||||
@@ -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<PollTimelineViewState,
|
||||
PollTimelineStateAction,
|
||||
PollTimelineViewAction>
|
||||
typealias TimelinePollViewModelType = StateStoreViewModel<TimelinePollViewState,
|
||||
TimelinePollStateAction,
|
||||
TimelinePollViewAction>
|
||||
@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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/UserSuggestion UserSuggestion
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/UserSuggestion UserSuggestion
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/UserSuggestion UserSuggestion
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/UserSuggestion UserSuggestion
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/UserSuggestion UserSuggestion
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/UserSuggestion UserSuggestion
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/UserSuggestion UserSuggestion
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/UserSuggestion UserSuggestion
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/UserSuggestion UserSuggestion
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/UserSuggestion UserSuggestion
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/UserSuggestion UserSuggestion
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/UserSuggestion UserSuggestion
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
// File created from SimpleUserProfileExample
|
||||
// $ createScreen.sh Room/UserSuggestion UserSuggestion
|
||||
//
|
||||
// Copyright 2021 New Vector Ltd
|
||||
//
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user