Merge pull request #5182 from vector-im/gil/143_create_public_space

Space creation element-ios
This commit is contained in:
Gil Eluard
2022-01-26 10:05:51 +01:00
committed by GitHub
177 changed files with 7919 additions and 21 deletions
@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "space_creation_camera.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "space_creation_camera@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "space_creation_camera@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 391 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 525 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 760 B

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "space_creation_private.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "space_creation_private@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "space_creation_private@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 384 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 597 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 865 B

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "space_creation_public.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "space_creation_public@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "space_creation_public@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 733 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 328 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 541 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 703 B

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "space_home_icon_dark.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "space_home_icon_dark@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "space_home_icon_dark@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 244 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 515 B

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "space_home_icon_light.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "space_home_icon_light@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "space_home_icon_light@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 540 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 685 B

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "spaces_add_space_dark.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "spaces_add_space_dark@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "spaces_add_space_dark@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 172 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 B

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "spaces_add_space_light.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "spaces_add_space_light@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "spaces_add_space_light@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 179 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 274 B

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "spaces_invite_users.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "spaces_invite_users@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "spaces_invite_users@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 474 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 803 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -1,17 +1,17 @@
{
"images" : [
{
"filename" : "space_home_icon.png",
"filename" : "spaces_modal_back.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "space_home_icon@2x.png",
"filename" : "spaces_modal_back@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "space_home_icon@3x.png",
"filename" : "spaces_modal_back@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 319 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 445 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 595 B

@@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "spaces_modal_close.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "spaces_modal_close@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "spaces_modal_close@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 363 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 552 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 720 B

+64
View File
@@ -66,6 +66,9 @@
"less" = "Less";
"open" = "Open";
"done" = "Done";
"private" = "Private";
"public" = "Public";
"stop" = "Stop";
"ok" = "OK";
// Call Bar
@@ -1749,6 +1752,8 @@ Tap the + to start adding people.";
"space_beta_announce_information" = "Spaces are a new way to group rooms and people. Theyre not on iOS yet, but you can use them now on Web and Desktop.";
"spaces_home_space_title" = "Home";
"spaces_add_space_title" = "Create space";
"spaces_create_space_title" = "Create a space";
"spaces_left_panel_title" = "Spaces";
"leave_space_title" = "Leave %@";
"leave_space_message" = "Are you sure you want to leave %@? Do you also want to leave all rooms and spaces of this space?";
@@ -1772,7 +1777,66 @@ Tap the + to start adding people.";
"space_home_show_all_rooms" = "Show all rooms";
"space_private_join_rule" = "Private space";
"space_private_join_rule_detail" = "Invite only, best for yourself or teams";
"space_public_join_rule" = "Public space";
"space_public_join_rule_detail" = "Open to anyone, best for communities";
"space_topic" = "description";
// Mark: - Space Creation
"spaces_creation_hint" = "Spaces are a new way to group rooms and people.";
"spaces_creation_visibility_title" = "What type of space do you want to create?";
"spaces_creation_visibility_message" = "To join an existing space, you need an invite.";
"spaces_creation_footer" = "You can change this later";
"spaces_creation_settings_message" = "Add some details to help it stand out. You can change these at any point.";
"spaces_creation_address" = "Address";
"spaces_creation_empty_room_name_error" = "Name required";
"spaces_creation_address_default_message" = "Your space will be viewable at\n%@";
"spaces_creation_address_invalid_characters" = "%@\nhas invalid characters";
"spaces_creation_address_already_exists" = "%@\nalready exists";
"spaces_creation_public_space_title" = "Your public space";
"spaces_creation_private_space_title" = "Your private space";
"spaces_creation_cancel_title" = "Stop creating a space?";
"spaces_creation_cancel_message" = "Your progress will be lost.";
"spaces_creation_new_rooms_title" = "What are some discussions youll have?";
"spaces_creation_new_rooms_message" = "Well create a room for each one.";
"spaces_creation_new_rooms_room_name_title" = "Room name";
"spaces_creation_new_rooms_general" = "General";
"spaces_creation_new_rooms_random" = "Random";
"spaces_creation_new_rooms_support" = "Support";
"spaces_creation_email_invites_title" = "Invite your team";
"spaces_creation_email_invites_message" = "You can invite them later too.";
"spaces_creation_email_invites_email_title" = "Email";
"spaces_creation_sharing_type_title" = "Who are you working with?";
"spaces_creation_sharing_type_message" = "Make sure the right people have access %@. You can change this later.";
"spaces_creation_sharing_type_just_me_title" = "Just me";
"spaces_creation_sharing_type_just_me_detail" = "A private space to organise your rooms";
"spaces_creation_sharing_type_me_and_teammates_title" = "Me and teammates";
"spaces_creation_sharing_type_me_and_teammates_detail" = "A private space for you & your teammates";
"spaces_creation_add_rooms_title" = "What do you want to add?";
"spaces_creation_add_rooms_message" = "As this space is just for you, no one will be informed. You can add more later.";
"spaces_creation_invite_by_username" = "Invite by username";
"spaces_creation_invite_by_username_title" = "Invite your team";
"spaces_creation_invite_by_username_message" = "You can invite them later too.";
"spaces_creation_post_process_creating_space" = "Creating space";
"spaces_creation_post_process_creating_space_task" = "Creating %@";
"spaces_creation_post_process_uploading_avatar" = "Uploading avatar";
"spaces_creation_post_process_creating_room" = "Creating %@";
"spaces_creation_post_process_adding_rooms" = "Adding %@ rooms";
"spaces_creation_post_process_inviting_users" = "Inviting %@ users";
"spaces_creation_in_spacename" = "in %@";
"spaces_creation_in_spacename_plus_one" = "in %@ + 1 space";
"spaces_creation_in_spacename_plus_many" = "in %@ + %@ spaces";
"spaces_creation_in_many_spaces" = "in %@ spaces";
"spaces_creation_in_one_space" = "in 1 space";
// Mark: Avatar
+10 -1
View File
@@ -206,7 +206,11 @@ internal enum Asset {
internal static let sideMenuNotifIcon = ImageAsset(name: "side_menu_notif_icon")
internal static let featureUnavaibleArtwork = ImageAsset(name: "feature_unavaible_artwork")
internal static let featureUnavaibleArtworkDark = ImageAsset(name: "feature_unavaible_artwork_dark")
internal static let spaceHomeIcon = ImageAsset(name: "space_home_icon")
internal static let spaceCreationCamera = ImageAsset(name: "space_creation_camera")
internal static let spaceCreationPrivate = ImageAsset(name: "space_creation_private")
internal static let spaceCreationPublic = ImageAsset(name: "space_creation_public")
internal static let spaceHomeIconDark = ImageAsset(name: "space_home_icon_dark")
internal static let spaceHomeIconLight = ImageAsset(name: "space_home_icon_light")
internal static let spaceMenuClose = ImageAsset(name: "space_menu_close")
internal static let spaceMenuLeave = ImageAsset(name: "space_menu_leave")
internal static let spaceMenuMembers = ImageAsset(name: "space_menu_members")
@@ -215,6 +219,11 @@ internal enum Asset {
internal static let spaceRoomIcon = ImageAsset(name: "space_room_icon")
internal static let spaceTypeIcon = ImageAsset(name: "space_type_icon")
internal static let spaceUserIcon = ImageAsset(name: "space_user_icon")
internal static let spacesAddSpaceDark = ImageAsset(name: "spaces_add_space_dark")
internal static let spacesAddSpaceLight = ImageAsset(name: "spaces_add_space_light")
internal static let spacesInviteUsers = ImageAsset(name: "spaces_invite_users")
internal static let spacesModalBack = ImageAsset(name: "spaces_modal_back")
internal static let spacesModalClose = ImageAsset(name: "spaces_modal_close")
internal static let spacesMore = ImageAsset(name: "spaces_more")
internal static let tabFavourites = ImageAsset(name: "tab_favourites")
internal static let tabGroups = ImageAsset(name: "tab_groups")
+212
View File
@@ -2631,6 +2631,14 @@ public class VectorL10n: NSObject {
public static var preview: String {
return VectorL10n.tr("Vector", "preview")
}
/// Private
public static var `private`: String {
return VectorL10n.tr("Vector", "private")
}
/// Public
public static var `public`: String {
return VectorL10n.tr("Vector", "public")
}
/// Public Rooms (at %@):
public static func publicRoomSectionTitle(_ p1: String) -> String {
return VectorL10n.tr("Vector", "public_room_section_title", p1)
@@ -5215,18 +5223,34 @@ public class VectorL10n: NSObject {
public static var spacePrivateJoinRule: String {
return VectorL10n.tr("Vector", "space_private_join_rule")
}
/// Invite only, best for yourself or teams
public static var spacePrivateJoinRuleDetail: String {
return VectorL10n.tr("Vector", "space_private_join_rule_detail")
}
/// Public space
public static var spacePublicJoinRule: String {
return VectorL10n.tr("Vector", "space_public_join_rule")
}
/// Open to anyone, best for communities
public static var spacePublicJoinRuleDetail: String {
return VectorL10n.tr("Vector", "space_public_join_rule_detail")
}
/// space
public static var spaceTag: String {
return VectorL10n.tr("Vector", "space_tag")
}
/// description
public static var spaceTopic: String {
return VectorL10n.tr("Vector", "space_topic")
}
/// Adding rooms coming soon
public static var spacesAddRoomsComingSoonTitle: String {
return VectorL10n.tr("Vector", "spaces_add_rooms_coming_soon_title")
}
/// Create space
public static var spacesAddSpaceTitle: String {
return VectorL10n.tr("Vector", "spaces_add_space_title")
}
/// This feature hasnt been implemented here, but its on the way. For now, you can do that with %@ on your computer.
public static func spacesComingSoonDetail(_ p1: String) -> String {
return VectorL10n.tr("Vector", "spaces_coming_soon_detail", p1)
@@ -5235,6 +5259,190 @@ public class VectorL10n: NSObject {
public static var spacesComingSoonTitle: String {
return VectorL10n.tr("Vector", "spaces_coming_soon_title")
}
/// Create a space
public static var spacesCreateSpaceTitle: String {
return VectorL10n.tr("Vector", "spaces_create_space_title")
}
/// As this space is just for you, no one will be informed. You can add more later.
public static var spacesCreationAddRoomsMessage: String {
return VectorL10n.tr("Vector", "spaces_creation_add_rooms_message")
}
/// What do you want to add?
public static var spacesCreationAddRoomsTitle: String {
return VectorL10n.tr("Vector", "spaces_creation_add_rooms_title")
}
/// Address
public static var spacesCreationAddress: String {
return VectorL10n.tr("Vector", "spaces_creation_address")
}
/// %@\nalready exists
public static func spacesCreationAddressAlreadyExists(_ p1: String) -> String {
return VectorL10n.tr("Vector", "spaces_creation_address_already_exists", p1)
}
/// Your space will be viewable at\n%@
public static func spacesCreationAddressDefaultMessage(_ p1: String) -> String {
return VectorL10n.tr("Vector", "spaces_creation_address_default_message", p1)
}
/// %@\nhas invalid characters
public static func spacesCreationAddressInvalidCharacters(_ p1: String) -> String {
return VectorL10n.tr("Vector", "spaces_creation_address_invalid_characters", p1)
}
/// Your progress will be lost.
public static var spacesCreationCancelMessage: String {
return VectorL10n.tr("Vector", "spaces_creation_cancel_message")
}
/// Stop creating a space?
public static var spacesCreationCancelTitle: String {
return VectorL10n.tr("Vector", "spaces_creation_cancel_title")
}
/// Email
public static var spacesCreationEmailInvitesEmailTitle: String {
return VectorL10n.tr("Vector", "spaces_creation_email_invites_email_title")
}
/// You can invite them later too.
public static var spacesCreationEmailInvitesMessage: String {
return VectorL10n.tr("Vector", "spaces_creation_email_invites_message")
}
/// Invite your team
public static var spacesCreationEmailInvitesTitle: String {
return VectorL10n.tr("Vector", "spaces_creation_email_invites_title")
}
/// Name required
public static var spacesCreationEmptyRoomNameError: String {
return VectorL10n.tr("Vector", "spaces_creation_empty_room_name_error")
}
/// You can change this later
public static var spacesCreationFooter: String {
return VectorL10n.tr("Vector", "spaces_creation_footer")
}
/// Spaces are a new way to group rooms and people.
public static var spacesCreationHint: String {
return VectorL10n.tr("Vector", "spaces_creation_hint")
}
/// in %@ spaces
public static func spacesCreationInManySpaces(_ p1: String) -> String {
return VectorL10n.tr("Vector", "spaces_creation_in_many_spaces", p1)
}
/// in 1 space
public static var spacesCreationInOneSpace: String {
return VectorL10n.tr("Vector", "spaces_creation_in_one_space")
}
/// in %@
public static func spacesCreationInSpacename(_ p1: String) -> String {
return VectorL10n.tr("Vector", "spaces_creation_in_spacename", p1)
}
/// in %@ + %@ spaces
public static func spacesCreationInSpacenamePlusMany(_ p1: String, _ p2: String) -> String {
return VectorL10n.tr("Vector", "spaces_creation_in_spacename_plus_many", p1, p2)
}
/// in %@ + 1 space
public static func spacesCreationInSpacenamePlusOne(_ p1: String) -> String {
return VectorL10n.tr("Vector", "spaces_creation_in_spacename_plus_one", p1)
}
/// Invite by username
public static var spacesCreationInviteByUsername: String {
return VectorL10n.tr("Vector", "spaces_creation_invite_by_username")
}
/// You can invite them later too.
public static var spacesCreationInviteByUsernameMessage: String {
return VectorL10n.tr("Vector", "spaces_creation_invite_by_username_message")
}
/// Invite your team
public static var spacesCreationInviteByUsernameTitle: String {
return VectorL10n.tr("Vector", "spaces_creation_invite_by_username_title")
}
/// General
public static var spacesCreationNewRoomsGeneral: String {
return VectorL10n.tr("Vector", "spaces_creation_new_rooms_general")
}
/// Well create a room for each one.
public static var spacesCreationNewRoomsMessage: String {
return VectorL10n.tr("Vector", "spaces_creation_new_rooms_message")
}
/// Random
public static var spacesCreationNewRoomsRandom: String {
return VectorL10n.tr("Vector", "spaces_creation_new_rooms_random")
}
/// Room name
public static var spacesCreationNewRoomsRoomNameTitle: String {
return VectorL10n.tr("Vector", "spaces_creation_new_rooms_room_name_title")
}
/// Support
public static var spacesCreationNewRoomsSupport: String {
return VectorL10n.tr("Vector", "spaces_creation_new_rooms_support")
}
/// What are some discussions youll have?
public static var spacesCreationNewRoomsTitle: String {
return VectorL10n.tr("Vector", "spaces_creation_new_rooms_title")
}
/// Adding %@ rooms
public static func spacesCreationPostProcessAddingRooms(_ p1: String) -> String {
return VectorL10n.tr("Vector", "spaces_creation_post_process_adding_rooms", p1)
}
/// Creating %@
public static func spacesCreationPostProcessCreatingRoom(_ p1: String) -> String {
return VectorL10n.tr("Vector", "spaces_creation_post_process_creating_room", p1)
}
/// Creating space
public static var spacesCreationPostProcessCreatingSpace: String {
return VectorL10n.tr("Vector", "spaces_creation_post_process_creating_space")
}
/// Creating %@
public static func spacesCreationPostProcessCreatingSpaceTask(_ p1: String) -> String {
return VectorL10n.tr("Vector", "spaces_creation_post_process_creating_space_task", p1)
}
/// Inviting %@ users
public static func spacesCreationPostProcessInvitingUsers(_ p1: String) -> String {
return VectorL10n.tr("Vector", "spaces_creation_post_process_inviting_users", p1)
}
/// Uploading avatar
public static var spacesCreationPostProcessUploadingAvatar: String {
return VectorL10n.tr("Vector", "spaces_creation_post_process_uploading_avatar")
}
/// Your private space
public static var spacesCreationPrivateSpaceTitle: String {
return VectorL10n.tr("Vector", "spaces_creation_private_space_title")
}
/// Your public space
public static var spacesCreationPublicSpaceTitle: String {
return VectorL10n.tr("Vector", "spaces_creation_public_space_title")
}
/// Add some details to help it stand out. You can change these at any point.
public static var spacesCreationSettingsMessage: String {
return VectorL10n.tr("Vector", "spaces_creation_settings_message")
}
/// A private space to organise your rooms
public static var spacesCreationSharingTypeJustMeDetail: String {
return VectorL10n.tr("Vector", "spaces_creation_sharing_type_just_me_detail")
}
/// Just me
public static var spacesCreationSharingTypeJustMeTitle: String {
return VectorL10n.tr("Vector", "spaces_creation_sharing_type_just_me_title")
}
/// A private space for you & your teammates
public static var spacesCreationSharingTypeMeAndTeammatesDetail: String {
return VectorL10n.tr("Vector", "spaces_creation_sharing_type_me_and_teammates_detail")
}
/// Me and teammates
public static var spacesCreationSharingTypeMeAndTeammatesTitle: String {
return VectorL10n.tr("Vector", "spaces_creation_sharing_type_me_and_teammates_title")
}
/// Make sure the right people have access %@. You can change this later.
public static func spacesCreationSharingTypeMessage(_ p1: String) -> String {
return VectorL10n.tr("Vector", "spaces_creation_sharing_type_message", p1)
}
/// Who are you working with?
public static var spacesCreationSharingTypeTitle: String {
return VectorL10n.tr("Vector", "spaces_creation_sharing_type_title")
}
/// To join an existing space, you need an invite.
public static var spacesCreationVisibilityMessage: String {
return VectorL10n.tr("Vector", "spaces_creation_visibility_message")
}
/// What type of space do you want to create?
public static var spacesCreationVisibilityTitle: String {
return VectorL10n.tr("Vector", "spaces_creation_visibility_title")
}
/// Some rooms may be hidden because theyre private and you need an invite.
public static var spacesEmptySpaceDetail: String {
return VectorL10n.tr("Vector", "spaces_empty_space_detail")
@@ -5279,6 +5487,10 @@ public class VectorL10n: NSObject {
public static var start: String {
return VectorL10n.tr("Vector", "start")
}
/// Stop
public static var stop: String {
return VectorL10n.tr("Vector", "stop")
}
/// Element is a new type of messenger and collaboration app that:\n\n1. Puts you in control to preserve your privacy\n2. Lets you communicate with anyone in the Matrix network, and even beyond by integrating with apps such as Slack\n3. Protects you from advertising, datamining, backdoors and walled gardens\n4. Secures you through end-to-end encryption, with cross-signing to verify others\n\nElement is completely different from other messaging and collaboration apps because it is decentralised and open source.\n\nElement lets you self-host - or choose a host - so that you have privacy, ownership and control of your data and conversations. It gives you access to an open network; so youre not just stuck speaking to other Element users only. And it is very secure.\n\nElement is able to do all this because it operates on Matrix - the standard for open, decentralised communication. \n\nElement puts you in control by letting you choose who hosts your conversations. From the Element app, you can choose to host in different ways:\n\n1. Get a free account on the matrix.org public server\n2. Self-host your account by running a server on your own hardware\n3. Sign up for an account on a custom server by simply subscribing to the Element Matrix Services hosting platform\n\nWhy choose Element?\n\nOWN YOUR DATA: You decide where to keep your data and messages. You own it and control it, not some MEGACORP that mines your data or gives access to third parties.\n\nOPEN MESSAGING AND COLLABORATION: You can chat with anyone else in the Matrix network, whether theyre using Element or another Matrix app, and even if they are using a different messaging system of the likes of Slack, IRC or XMPP.\n\nSUPER-SECURE: Real end-to-end encryption (only those in the conversation can decrypt messages), and cross-signing to verify the devices of conversation participants.\n\nCOMPLETE COMMUNICATION: Messaging, voice and video calls, file sharing, screen sharing and a whole bunch of integrations, bots and widgets. Build rooms, communities, stay in touch and get things done.\n\nEVERYWHERE YOU ARE: Stay in touch wherever you are with fully synchronised message history across all your devices and on the web at https://element.io/app.
public static var storeFullDescription: String {
return VectorL10n.tr("Vector", "store_full_description")
@@ -26,6 +26,8 @@ class VectorHostingController: UIHostingController<AnyView> {
// MARK: Private
var isNavigationBarHidden: Bool = false
var hidesBackTitleWhenPushed: Bool = false
private var theme: Theme
init<Content>(rootView: Content) where Content: View {
@@ -48,6 +50,26 @@ class VectorHostingController: UIHostingController<AnyView> {
self.update(theme: self.theme)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
if isNavigationBarHidden {
self.navigationController?.isNavigationBarHidden = true
}
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if hidesBackTitleWhenPushed {
vc_removeBackTitle()
}
if navigationController?.isNavigationBarHidden ?? false {
navigationController?.interactivePopGestureRecognizer?.delegate = nil
}
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
@@ -64,6 +64,7 @@ final class SideMenuCoordinator: NSObject, SideMenuCoordinatorType {
private var exploreRoomCoordinator: ExploreRoomCoordinator?
private var membersCoordinator: SpaceMembersCoordinator?
private var createSpaceCoordinator: SpaceCreationCoordinator?
// MARK: Public
@@ -257,6 +258,36 @@ final class SideMenuCoordinator: NSObject, SideMenuCoordinatorType {
self.spaceDetailPresenter.present(forSpaceWithId: spaceId, from: self.sideMenuViewController, sourceView: sourceView, session: session, animated: true)
}
@available(iOS 14.0, *)
private func showCreateSpace() {
guard let session = self.parameters.userSessionsService.mainUserSession?.matrixSession else {
return
}
let coordinator = SpaceCreationCoordinator(parameters: SpaceCreationCoordinatorParameters(session: session))
let presentable = coordinator.toPresentable()
presentable.presentationController?.delegate = self
self.sideMenuViewController.present(presentable, animated: true, completion: nil)
coordinator.callback = { [weak self] result in
guard let self = self else {
return
}
self.createSpaceCoordinator?.toPresentable().dismiss(animated: true) {
self.createSpaceCoordinator = nil
switch result {
case .cancel:
break
case .done(let spaceId):
self.select(spaceWithId: spaceId)
}
}
}
coordinator.start()
self.createSpaceCoordinator = coordinator
}
// MARK: UserSessions management
private func registerUserSessionsServiceNotifications() {
@@ -310,7 +341,7 @@ extension SideMenuCoordinator: SideMenuNavigationControllerDelegate {
// MARK: - SideMenuNavigationControllerDelegate
extension SideMenuCoordinator: SpaceListCoordinatorDelegate {
func spaceListCoordinatorDidSelectHomeSpace(_ coordinator: SpaceListCoordinatorType) {
func spaceListCoordinatorDidSelectHomeSpace(_ coordinator: SpaceListCoordinatorType) {
self.parameters.appNavigator.sideMenu.dismiss(animated: true) {
}
@@ -331,6 +362,12 @@ extension SideMenuCoordinator: SpaceListCoordinatorDelegate {
func spaceListCoordinator(_ coordinator: SpaceListCoordinatorType, didPressMoreForSpaceWithId spaceId: String, from sourceView: UIView) {
self.showMenu(forSpaceWithId: spaceId, from: sourceView)
}
func spaceListCoordinatorDidSelectCreateSpace(_ coordinator: SpaceListCoordinatorType) {
if #available(iOS 14.0, *) {
self.showCreateSpace()
}
}
}
// MARK: - SpaceMenuPresenterDelegate
@@ -386,5 +423,6 @@ extension SideMenuCoordinator: UIAdaptivePresentationControllerDelegate {
func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
self.exploreRoomCoordinator = nil
self.membersCoordinator = nil
self.createSpaceCoordinator = nil
}
}
@@ -105,7 +105,6 @@ final class SideMenuViewModel: SideMenuViewModelType {
sideMenuItems += [
.settings,
.help,
.feedback
]
@@ -85,4 +85,9 @@ extension SpaceListCoordinator: SpaceListViewModelCoordinatorDelegate {
func spaceListViewModel(_ viewModel: SpaceListViewModelType, didPressMoreForSpaceWithId spaceId: String, from sourceView: UIView) {
self.delegate?.spaceListCoordinator(self, didPressMoreForSpaceWithId: spaceId, from: sourceView)
}
func spaceListViewModelDidSelectCreateSpace(_ viewModel: SpaceListViewModelType) {
self.delegate?.spaceListCoordinatorDidSelectCreateSpace(self)
}
}
@@ -23,6 +23,7 @@ protocol SpaceListCoordinatorDelegate: AnyObject {
func spaceListCoordinator(_ coordinator: SpaceListCoordinatorType, didSelectSpaceWithId spaceId: String)
func spaceListCoordinator(_ coordinator: SpaceListCoordinatorType, didSelectInviteWithId spaceId: String, from sourceView: UIView?)
func spaceListCoordinator(_ coordinator: SpaceListCoordinatorType, didPressMoreForSpaceWithId spaceId: String, from sourceView: UIView)
func spaceListCoordinatorDidSelectCreateSpace(_ coordinator: SpaceListCoordinatorType)
}
/// `SpaceListCoordinatorType` is a protocol describing a Coordinator that handle key backup setup passphrase navigation flow.
@@ -20,4 +20,5 @@ import Foundation
enum SpaceListSection {
case home(_ viewData: SpaceListItemViewData)
case spaces(_ viewDataList: [SpaceListItemViewData])
case addSpace(_ viewData: SpaceListItemViewData)
}
@@ -58,7 +58,7 @@ final class SpaceListViewCell: UITableViewCell, Themable, NibReusable {
func fill(with viewData: SpaceListItemViewData) {
self.avatarView.fill(with: viewData.avatarViewData)
self.titleLabel.text = viewData.title
self.moreButton.isHidden = viewData.isInvite
self.moreButton.isHidden = viewData.spaceId == SpaceListViewModel.Constants.addSpaceId || viewData.isInvite
if viewData.isInvite {
self.isBadgeAlert = true
self.badgeLabel.isHidden = false
@@ -68,7 +68,7 @@ final class SpaceListViewCell: UITableViewCell, Themable, NibReusable {
self.badgeLabel.text = "!"
} else {
self.isBadgeAlert = viewData.highlightedNotificationCount > 0
let notificationCount = viewData.notificationCount + viewData.highlightedNotificationCount
let notificationCount = viewData.notificationCount
self.badgeLabel.isHidden = notificationCount == 0
if let theme = self.theme {
self.badgeLabel.badgeColor = viewData.highlightedNotificationCount == 0 ? theme.colors.tertiaryContent : theme.colors.alert
@@ -172,8 +172,10 @@ extension SpaceListViewController: UITableViewDataSource {
numberOfRows = 1
case .spaces(let viewDataList):
numberOfRows = viewDataList.count
case .addSpace:
numberOfRows = 1
}
return numberOfRows
}
@@ -189,6 +191,8 @@ extension SpaceListViewController: UITableViewDataSource {
viewData = spaceViewData
case .spaces(let viewDataList):
viewData = viewDataList[indexPath.row]
case .addSpace(let spaceViewData):
viewData = spaceViewData
}
cell.update(theme: self.theme)
@@ -24,6 +24,7 @@ final class SpaceListViewModel: SpaceListViewModelType {
enum Constants {
static let homeSpaceId: String = "home"
static let addSpaceId: String = "add_space"
}
// MARK: - Properties
@@ -55,6 +56,8 @@ final class SpaceListViewModel: SpaceListViewModelType {
NotificationCenter.default.addObserver(self, selector: #selector(self.sessionDidSync(notification:)), name: MXSpaceService.didBuildSpaceGraph, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.counterDidUpdateNotificationCount(notification:)), name: MXSpaceNotificationCounter.didUpdateNotificationCount, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.loadData), name: NSNotification.Name.themeServiceDidChangeTheme, object: nil)
}
@@ -87,12 +90,16 @@ final class SpaceListViewModel: SpaceListViewModelType {
self.selectedIndexPath = indexPath
self.update(viewState: .selectionChanged(indexPath))
}
case .addSpace:
self.update(viewState: .selectionChanged(self.selectedIndexPath))
addSpace()
}
case .moreAction(at: let indexPath, from: let sourceView):
let section = self.sections[indexPath.section]
switch section {
case .home:
self.coordinatorDelegate?.spaceListViewModel(self, didPressMoreForSpaceWithId: Constants.homeSpaceId, from: sourceView)
case .addSpace: break
case .spaces(let viewDataList):
let spaceViewData = viewDataList[indexPath.row]
self.coordinatorDelegate?.spaceListViewModel(self, didPressMoreForSpaceWithId: spaceViewData.spaceId, from: sourceView)
@@ -108,6 +115,7 @@ final class SpaceListViewModel: SpaceListViewModelType {
for (sectionIndex, section) in self.sections.enumerated() {
switch section {
case .home: break
case .addSpace: break
case .spaces(let viewDataList):
for (row, itemViewData) in viewDataList.enumerated() where itemViewData.spaceId == spaceId {
let indexPath = IndexPath(row: row, section: sectionIndex)
@@ -129,7 +137,7 @@ final class SpaceListViewModel: SpaceListViewModelType {
loadData()
}
private func loadData() {
@objc private func loadData() {
guard let session = self.userSessionsService.mainUserSession?.matrixSession else {
// If there is no main session, reset current selection and give an empty section list
// It can happen when the user make a clear cache or logout
@@ -142,7 +150,7 @@ final class SpaceListViewModel: SpaceListViewModelType {
let homeViewData = self.createHomeViewData(session: session)
let viewDataList = getSpacesViewData(session: session)
let sections: [SpaceListSection] = viewDataList.invites.isEmpty ? [
var sections: [SpaceListSection] = viewDataList.invites.isEmpty ? [
.home(homeViewData),
.spaces(viewDataList.spaces)
]
@@ -152,6 +160,12 @@ final class SpaceListViewModel: SpaceListViewModelType {
.home(homeViewData),
.spaces(viewDataList.spaces)
]
let spacesSectionIndex = sections.count - 1
if #available(iOS 14.0, *) {
let addSpaceViewData = self.createAddSpaceViewData(session: session)
sections.append(.addSpace(addSpaceViewData))
}
self.sections = sections
let homeIndexPath = viewDataList.invites.isEmpty ? IndexPath(row: 0, section: 0) : IndexPath(row: 0, section: 1)
@@ -159,10 +173,9 @@ final class SpaceListViewModel: SpaceListViewModelType {
self.selectedIndexPath = homeIndexPath
} else if self.selectedItemId != self.itemId(with: self.selectedIndexPath) {
var newSelection: IndexPath?
let section = sections.last
let section = sections[spacesSectionIndex]
switch section {
case .home:
break
case .home, .addSpace: break
case .spaces(let viewDataList):
var index = 0
for itemViewData in viewDataList {
@@ -171,8 +184,6 @@ final class SpaceListViewModel: SpaceListViewModelType {
}
index += 1
}
case .none:
break
}
if let selection = newSelection {
@@ -191,6 +202,10 @@ final class SpaceListViewModel: SpaceListViewModelType {
self.coordinatorDelegate?.spaceListViewModelDidSelectHomeSpace(self)
}
private func addSpace() {
self.coordinatorDelegate?.spaceListViewModelDidSelectCreateSpace(self)
}
private func selectSpace(with spaceId: String) {
self.coordinatorDelegate?.spaceListViewModel(self, didSelectSpaceWithId: spaceId)
}
@@ -200,21 +215,44 @@ final class SpaceListViewModel: SpaceListViewModelType {
}
private func createHomeViewData(session: MXSession) -> SpaceListItemViewData {
let avatarViewData = AvatarViewData(matrixItemId: Constants.homeSpaceId, displayName: nil, avatarUrl: nil, mediaManager: session.mediaManager, fallbackImage: .image(Asset.Images.spaceHomeIcon.image, .center))
let defaultAsset = ThemeService.shared().isCurrentThemeDark() ? Asset.Images.spaceHomeIconDark : Asset.Images.spaceHomeIconLight
let avatarViewData = AvatarViewData(matrixItemId: Constants.homeSpaceId, displayName: nil, avatarUrl: nil, mediaManager: session.mediaManager, fallbackImage: .image(defaultAsset.image, .center))
let homeNotificationState = session.spaceService.notificationCounter.homeNotificationState
let homeViewData = SpaceListItemViewData(spaceId: Constants.homeSpaceId,
title: VectorL10n.spacesHomeSpaceTitle, avatarViewData: avatarViewData, isInvite: false, notificationCount: homeNotificationState.allCount, highlightedNotificationCount: homeNotificationState.allHighlightCount)
title: VectorL10n.spacesHomeSpaceTitle,
avatarViewData: avatarViewData,
isInvite: false,
notificationCount: homeNotificationState.allCount,
highlightedNotificationCount: homeNotificationState.allHighlightCount)
return homeViewData
}
private func createAddSpaceViewData(session: MXSession) -> SpaceListItemViewData {
let defaultAsset = ThemeService.shared().isCurrentThemeDark() ? Asset.Images.spacesAddSpaceDark : Asset.Images.spacesAddSpaceLight
let avatarViewData = AvatarViewData(matrixItemId: Constants.addSpaceId, displayName: nil, avatarUrl: nil, mediaManager: session.mediaManager, fallbackImage: .image(defaultAsset.image, .center))
let homeViewData = SpaceListItemViewData(spaceId: Constants.addSpaceId,
title: VectorL10n.spacesAddSpaceTitle,
avatarViewData: avatarViewData,
isInvite: false,
notificationCount: 0,
highlightedNotificationCount: 0)
return homeViewData
}
private func getSpacesViewData(session: MXSession) -> (invites: [SpaceListItemViewData], spaces: [SpaceListItemViewData]) {
var invites: [SpaceListItemViewData] = []
var spaces: [SpaceListItemViewData] = []
session.spaceService.rootSpaceSummaries.forEach { summary in
let avatarViewData = AvatarViewData(matrixItemId: summary.roomId, displayName: summary.displayname, avatarUrl: summary.avatar, mediaManager: session.mediaManager, fallbackImage: .matrixItem(summary.roomId, summary.displayname))
let notificationState = session.spaceService.notificationCounter.notificationState(forSpaceWithId: summary.roomId)
let viewData = SpaceListItemViewData(spaceId: summary.roomId, title: summary.displayname, avatarViewData: avatarViewData, isInvite: summary.membership == .invite, notificationCount: notificationState?.groupMissedDiscussionsCount ?? 0, highlightedNotificationCount: notificationState?.groupMissedDiscussionsHighlightedCount ?? 0)
let viewData = SpaceListItemViewData(spaceId: summary.roomId,
title: summary.displayname,
avatarViewData: avatarViewData,
isInvite: summary.membership == .invite,
notificationCount: notificationState?.groupMissedDiscussionsCount ?? 0,
highlightedNotificationCount: notificationState?.groupMissedDiscussionsHighlightedCount ?? 0)
if viewData.isInvite {
invites.append(viewData)
} else {
@@ -244,6 +282,8 @@ final class SpaceListViewModel: SpaceListViewModelType {
case .spaces(let viewDataList):
let spaceViewData = viewDataList[self.selectedIndexPath.row]
return spaceViewData.spaceId
case .addSpace:
return Constants.addSpaceId
}
}
@@ -27,6 +27,7 @@ protocol SpaceListViewModelCoordinatorDelegate: AnyObject {
func spaceListViewModel(_ viewModel: SpaceListViewModelType, didSelectSpaceWithId spaceId: String)
func spaceListViewModel(_ viewModel: SpaceListViewModelType, didSelectInviteWithId spaceId: String, from sourceView: UIView?)
func spaceListViewModel(_ viewModel: SpaceListViewModelType, didPressMoreForSpaceWithId spaceId: String, from sourceView: UIView)
func spaceListViewModelDidSelectCreateSpace(_ viewModel: SpaceListViewModelType)
}
/// Protocol describing the view model used by `SpaceListViewController`
@@ -103,6 +103,12 @@ class SpaceMenuViewController: UIViewController {
self.subtitleLabel.font = theme.fonts.caption1
self.closeButton.backgroundColor = theme.roomInputTextBorder
self.closeButton.tintColor = theme.noticeSecondaryColor
if self.spaceId == SpaceListViewModel.Constants.homeSpaceId {
let defaultAsset = ThemeService.shared().isCurrentThemeDark() ? Asset.Images.spaceHomeIconDark : Asset.Images.spaceHomeIconLight
let avatarViewData = AvatarViewData(matrixItemId: self.spaceId, displayName: nil, avatarUrl: nil, mediaManager: session.mediaManager, fallbackImage: .image(defaultAsset.image, .center))
self.avatarView.fill(with: avatarViewData)
}
}
private func registerThemeServiceDidChangeThemeNotification() {
@@ -117,8 +123,6 @@ class SpaceMenuViewController: UIViewController {
setupTableView()
if self.spaceId == SpaceListViewModel.Constants.homeSpaceId {
let avatarViewData = AvatarViewData(matrixItemId: self.spaceId, displayName: nil, avatarUrl: nil, mediaManager: session.mediaManager, fallbackImage: .image(Asset.Images.spaceHomeIcon.image, .center))
self.avatarView.fill(with: avatarViewData)
self.titleLabel.text = VectorL10n.titleHome
self.subtitleLabel.text = VectorL10n.settingsTitle
@@ -0,0 +1,109 @@
//
// 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 DesignKit
@available(iOS 14.0, *)
struct SpaceAvatarImage: View {
@Environment(\.theme) var theme: ThemeSwiftUI
@Environment(\.dependencies) var dependencies: DependencyContainer
@StateObject var viewModel = AvatarViewModel()
var mxContentUri: String?
var matrixItemId: String
var displayName: String?
var size: AvatarSize
var body: some View {
Group {
switch viewModel.viewState {
case .empty:
ProgressView()
case .placeholder(let firstCharacter, let colorIndex):
Text(firstCharacter)
.padding(10)
.frame(width: CGFloat(size.rawValue), height: CGFloat(size.rawValue))
.foregroundColor(.white)
.background(theme.colors.namesAndAvatars[colorIndex])
.clipShape(RoundedRectangle(cornerRadius: 8))
// Make the text resizable (i.e. Make it large and then allow it to scale down)
.font(.system(size: 200))
.minimumScaleFactor(0.001)
case .avatar(let image):
Image(uiImage: image)
.resizable()
.frame(width: CGFloat(size.rawValue), height: CGFloat(size.rawValue))
.clipShape(Circle())
}
}
.onChange(of: displayName, perform: { value in
viewModel.loadAvatar(
mxContentUri: mxContentUri,
matrixItemId: matrixItemId,
displayName: value,
colorCount: theme.colors.namesAndAvatars.count,
avatarSize: size
)
})
.onAppear {
viewModel.inject(dependencies: dependencies)
viewModel.loadAvatar(
mxContentUri: mxContentUri,
matrixItemId: matrixItemId,
displayName: displayName,
colorCount: theme.colors.namesAndAvatars.count,
avatarSize: size
)
}
}
}
@available(iOS 14.0, *)
extension SpaceAvatarImage {
init(avatarData: AvatarInputProtocol, size: AvatarSize) {
self.init(
mxContentUri: avatarData.mxContentUri,
matrixItemId: avatarData.matrixItemId,
displayName: avatarData.displayName,
size: size
)
}
}
@available(iOS 14.0, *)
struct LiveAvatarImage_Previews: PreviewProvider {
static let mxContentUri = "fakeUri"
static let name = "Alice"
static var previews: some View {
Group {
HStack {
VStack(alignment: .center, spacing: 20) {
SpaceAvatarImage(avatarData: MockAvatarInput.example, size: .xSmall)
SpaceAvatarImage(avatarData: MockAvatarInput.example, size: .medium)
SpaceAvatarImage(avatarData: MockAvatarInput.example, size: .xLarge)
}
VStack(alignment: .center, spacing: 20) {
SpaceAvatarImage(mxContentUri: nil, matrixItemId: name, displayName: name, size: .xSmall)
SpaceAvatarImage(mxContentUri: nil, matrixItemId: name, displayName: name, size: .medium)
SpaceAvatarImage(mxContentUri: nil, matrixItemId: name, displayName: name, size: .xLarge)
}
}
.addDependency(MockAvatarService.example)
}
}
}
@@ -0,0 +1,31 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
extension MXUser: Avatarable {
var mxContentUri: String? {
avatarUrl
}
var matrixItemId: String {
userId
}
var displayName: String? {
displayname
}
}
@@ -24,6 +24,12 @@ enum MockAppScreens {
MockAnalyticsPromptScreenState.self,
MockUserSuggestionScreenState.self,
MockPollEditFormScreenState.self,
MockSpaceCreationEmailInvitesScreenState.self,
MockSpaceCreationMatrixItemChooserScreenState.self,
MockSpaceCreationMenuScreenState.self,
MockSpaceCreationRoomsScreenState.self,
MockSpaceCreationSettingsScreenState.self,
MockSpaceCreationPostProcessScreenState.self,
MockTimelinePollScreenState.self,
MockTemplateUserProfileScreenState.self,
MockTemplateRoomListScreenState.self,
@@ -0,0 +1,53 @@
//
// 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
/// `ClearViewModifier` aims to add a clear button (e.g. `x` button) on the right side of any text editing view
@available(iOS 14.0, *)
struct ClearViewModifier: ViewModifier
{
// MARK: - Properties
let alignment: VerticalAlignment
// MARK: - Bindings
@Binding var text: String
// MARK: - Private
@Environment(\.theme) private var theme: ThemeSwiftUI
// MARK: - Public
public func body(content: Content) -> some View
{
HStack(alignment: alignment) {
content
if !text.isEmpty {
Button(action: {
self.text = ""
}) {
Image(systemName: "xmark.circle.fill")
.renderingMode(.template)
.foregroundColor(theme.colors.quarterlyContent)
}
.padding(EdgeInsets(top: alignment == .top ? 8 : 0, leading: 0, bottom: alignment == .bottom ? 8 : 0, trailing: 8))
}
}
}
}
@@ -0,0 +1,90 @@
//
// 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 OptionButton: View {
// MARK: - Style
private struct Style: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.scaleEffect(configuration.isPressed ? 0.97 : 1)
.animation(.easeOut(duration: 0.2), value: configuration.isPressed)
}
}
// MARK: - Properties
let icon: UIImage?
let title: String
let detailMessage: String?
let action: () -> Void
// MARK: Private
@Environment(\.theme) private var theme: ThemeSwiftUI
// MARK: Public
var body: some View {
Button(action: action, label: {
HStack {
if let image = icon {
Image(uiImage: image).renderingMode(.template).resizable().frame(width: 24, height: 24).foregroundColor(theme.colors.secondaryContent)
}
VStack(alignment: .leading, spacing: nil) {
Text(title).font(theme.fonts.bodySB).foregroundColor(theme.colors.primaryContent)
if let detail = detailMessage {
Text(detail).font(theme.fonts.caption1).foregroundColor(theme.colors.secondaryContent)
}
}
Spacer()
Image(systemName: "chevron.right").font(.system(size: 16, weight: .regular)).foregroundColor(theme.colors.quarterlyContent)
}
.padding(EdgeInsets(top: 15, leading: 16, bottom: 15, trailing: 16))
.background(theme.colors.quinaryContent)
.foregroundColor(theme.colors.secondaryContent)
.clipShape(RoundedRectangle(cornerRadius: 8))
}
)
.buttonStyle(Style())
}
}
// MARK: - Previews
@available(iOS 14.0, *)
struct OptionButton_Previews: PreviewProvider {
static var previews: some View {
Group {
VStack {
OptionButton(icon: Asset.Images.spaceTypeIcon.image, title: "A title", detailMessage: "Some details for this option", action: {}).theme(.light)
OptionButton(icon: nil, title: "A title", detailMessage: "Some details for this option", action: {}).theme(.light)
OptionButton(icon: nil, title: "A title", detailMessage: nil, action: {}).theme(.light)
}
VStack {
OptionButton(icon: Asset.Images.spaceTypeIcon.image, title: "A title", detailMessage: "Some details for this option", action: {}).theme(.dark)
OptionButton(icon: nil, title: "A title", detailMessage: "Some details for this option", action: {}).theme(.dark)
OptionButton(icon: nil, title: "A title", detailMessage: nil, action: {}).theme(.dark)
}.preferredColorScheme(.dark)
}
.padding()
}
}
@@ -0,0 +1,93 @@
//
// 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
/// `ResponderManager` is used to chain `SwiftUI` text editing views that embed `UIKit` text editing views using `UIViewRepresentable`
class ResponderManager {
private static var tagIndex: Int = 1000
private static var registeredResponders = NSMapTable<NSNumber, UIView>(keyOptions: .strongMemory, valueOptions: .weakMemory)
private static var nextIndex: Int {
tagIndex += 1
return tagIndex
}
private static var firstResponder: UIView? {
guard let enumerator = registeredResponders.objectEnumerator() else {
return nil
}
while let view: UIView = enumerator.nextObject() as? UIView {
if view.isFirstResponder {
return view
}
}
return nil
}
/// register the `UIKit` view as a responder
///
/// - Parameters:
/// - view: view to be registered
static func register(view: UIView) {
if registeredResponders.object(forKey: NSNumber(value: view.tag)) == nil {
view.tag = nextIndex
registeredResponders.setObject(view, forKey: NSNumber(value: view.tag))
}
}
/// Unregister the `UIKit` view from this manager. The view won't be considered as potential next responder anymore
///
/// - Parameters:
/// - view: view to be unregistered
static func unregister(view: UIView) {
registeredResponders.removeObject(forKey: NSNumber(value: view.tag))
}
/// Tries to get the focused registered responder and give the focus to it's next responder
/// - Returns: `True` if the next responder has been found and is successfully focused. `False` otherwise.
static func makeActiveNextResponder() -> Bool {
guard let firstResponder = self.firstResponder else {
return false
}
return makeActiveNextResponder(of: firstResponder)
}
/// Give the focus to the next responder f the given `UIKit` view
///
/// - Parameters:
/// - view: base view
///
/// - Returns: `True` if the next responder has been found and is successfully focused. `False` otherwise.
static func makeActiveNextResponder(of view: UIView) -> Bool {
let nextTag = view.tag + 1
guard let nextResponder = registeredResponders.object(forKey: NSNumber(value: nextTag)) else {
return false
}
nextResponder.becomeFirstResponder()
return true
}
/// Unfocus any focused registered view.
static func resignFirstResponder() {
firstResponder?.resignFirstResponder()
}
}
@@ -0,0 +1,125 @@
//
// 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 RoundedBorderTextEditor: View {
// MARK: - Properties
var title: String?
var placeHolder: String
@Binding var text: String
var textMaxHeight: CGFloat?
@Binding var error: String?
var onTextChanged: ((String) -> Void)?
var onEditingChanged: ((Bool) -> Void)?
@State private var editing = false
// MARK: Private
@Environment(\.theme) private var theme: ThemeSwiftUI
// MARK: Setup
init(title: String? = nil,
placeHolder: String,
text: Binding<String>,
textMaxHeight: CGFloat? = nil,
error: Binding<String?> = .constant(nil),
onTextChanged: ((String) -> Void)? = nil,
onEditingChanged: ((Bool) -> Void)? = nil) {
self.title = title
self.placeHolder = placeHolder
self._text = text
self.textMaxHeight = textMaxHeight
self._error = error
self.onTextChanged = onTextChanged
self.onEditingChanged = onEditingChanged
}
// MARK: Public
var body: some View {
VStack(alignment: .leading, spacing: -1) {
if let title = self.title {
Text(title)
.foregroundColor(theme.colors.primaryContent)
.font(theme.fonts.subheadline)
.multilineTextAlignment(.leading)
.padding(EdgeInsets(top: 0, leading: 0, bottom: 8, trailing: 0))
}
ZStack(alignment: .topLeading) {
if text.isEmpty {
Text(placeHolder)
.padding(EdgeInsets(top: 10, leading: 10, bottom: 0, trailing: 0))
.font(theme.fonts.callout)
.foregroundColor(theme.colors.tertiaryContent)
.allowsHitTesting(false)
}
ThemableTextEditor(text: $text, onEditingChanged: { edit in
self.editing = edit
onEditingChanged?(edit)
})
.modifier(ClearViewModifier(alignment: .top, text: $text))
// Found no good solution here. Hidding next button for the moment
// .modifier(NextViewModifier(alignment: .bottomTrailing, isEditing: $editing))
.padding(EdgeInsets(top: 2, leading: 6, bottom: 0, trailing: 0))
.onChange(of: text, perform: { newText in
onTextChanged?(newText)
})
}
.overlay(RoundedRectangle(cornerRadius: 8)
.stroke(editing ? theme.colors.accent : (error == nil ? theme.colors.quinaryContent : theme.colors.alert), lineWidth: editing || error != nil ? 2 : 1))
.frame(height: textMaxHeight)
if let error = self.error {
Text(error)
.foregroundColor(theme.colors.alert)
.font(theme.fonts.footnote)
.multilineTextAlignment(.leading)
.padding(EdgeInsets(top: 8, leading: 0, bottom: 0, trailing: 0))
.transition(.opacity)
}
}
.animation(.easeOut(duration: 0.2), value: error)
}
}
// MARK: - Previews
@available(iOS 14.0, *)
struct ThemableTextEditor_Previews: PreviewProvider {
static var previews: some View {
Group {
VStack(alignment: .center, spacing: 40) {
RoundedBorderTextEditor(title: "A title", placeHolder: "A placeholder", text: .constant(""), error: .constant(nil))
RoundedBorderTextEditor(placeHolder: "A placeholder", text: .constant("Some text"), error: .constant(nil))
RoundedBorderTextEditor(title: "A title", placeHolder: "A placeholder", text: .constant("Some very long text used to check overlapping with the delete button"), error: .constant("Some error text"))
}
VStack(alignment: .center, spacing: 40) {
RoundedBorderTextEditor(title: "A title", placeHolder: "A placeholder", text: .constant(""), error: .constant(nil))
RoundedBorderTextEditor(placeHolder: "A placeholder", text: .constant("Some text"), error: .constant(nil))
RoundedBorderTextEditor(title: "A title", placeHolder: "A placeholder", text: .constant("Some text"), error: .constant("Some error text"))
}
.theme(.dark).preferredColorScheme(.dark)
}
.padding()
}
}
@@ -0,0 +1,126 @@
//
// 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 RoundedBorderTextField: View {
// MARK: - Properties
var title: String?
var placeHolder: String
@Binding var text: String
@Binding var footerText: String?
@Binding var isError: Bool
var isFirstResponder = false
var configuration: UIKitTextInputConfiguration = UIKitTextInputConfiguration()
var onTextChanged: ((String) -> Void)?
var onEditingChanged: ((Bool) -> Void)?
// MARK: Private
@State private var editing = false
@Environment(\.theme) private var theme: ThemeSwiftUI
// MARK: Setup
init(title: String? = nil, placeHolder: String, text: Binding<String>, footerText: Binding<String?> = .constant(nil), isError: Binding<Bool> = .constant(false), isFirstResponder: Bool = false, configuration: UIKitTextInputConfiguration = UIKitTextInputConfiguration(), onTextChanged: ((String) -> Void)? = nil, onEditingChanged: ((Bool) -> Void)? = nil) {
self.title = title
self.placeHolder = placeHolder
self._text = text
self._footerText = footerText
self._isError = isError
self.isFirstResponder = isFirstResponder
self.configuration = configuration
self.onTextChanged = onTextChanged
self.onEditingChanged = onEditingChanged
}
// MARK: Public
var body: some View {
VStack(alignment: .leading, spacing: -1) {
if let title = self.title {
Text(title)
.foregroundColor(theme.colors.primaryContent)
.font(theme.fonts.subheadline)
.multilineTextAlignment(.leading)
.padding(EdgeInsets(top: 0, leading: 0, bottom: 8, trailing: 0))
}
ZStack(alignment: .leading) {
if text.isEmpty {
Text(placeHolder)
.font(theme.fonts.callout)
.foregroundColor(theme.colors.tertiaryContent)
.lineLimit(1)
}
ThemableTextField(placeholder: "", text: $text, configuration: configuration, onEditingChanged: { edit in
self.editing = edit
onEditingChanged?(edit)
})
.makeFirstResponder(isFirstResponder)
.onChange(of: text, perform: { newText in
onTextChanged?(newText)
})
.frame(height: 30)
.modifier(ClearViewModifier(alignment: .center, text: $text))
}
.padding(EdgeInsets(top: 8, leading: 8, bottom: 8, trailing: text.isEmpty ? 8 : 0))
.overlay(RoundedRectangle(cornerRadius: 8)
.stroke(editing ? theme.colors.accent : (footerText != nil && isError ? theme.colors.alert : theme.colors.quinaryContent), lineWidth: editing || (footerText != nil && isError) ? 2 : 1))
if let footerText = self.footerText {
Text(footerText)
.foregroundColor(isError ? theme.colors.alert : theme.colors.tertiaryContent)
.font(theme.fonts.footnote)
.multilineTextAlignment(.leading)
.padding(EdgeInsets(top: 8, leading: 0, bottom: 0, trailing: 0))
.transition(.opacity)
}
}
.animation(.easeOut(duration: 0.2))
}
}
// MARK: - Previews
@available(iOS 14.0, *)
struct TextFieldWithError_Previews: PreviewProvider {
static var previews: some View {
Group {
VStack(alignment: .center, spacing: 40) {
RoundedBorderTextField(title: "A title", placeHolder: "A placeholder", text: .constant(""), footerText: .constant(nil), isError: .constant(false))
RoundedBorderTextField(placeHolder: "A placeholder", text: .constant("Some text"), footerText: .constant(nil), isError: .constant(false))
RoundedBorderTextField(title: "A title", placeHolder: "A placeholder", text: .constant("Some very long text used to check overlapping with the delete button"), footerText: .constant("Some error text"), isError: .constant(true))
RoundedBorderTextField(title: "A title", placeHolder: "A placeholder", text: .constant("Some very long text used to check overlapping with the delete button"), footerText: .constant("Some normal text"), isError: .constant(false))
}
VStack(alignment: .center, spacing: 20) {
RoundedBorderTextField(title: "A title", placeHolder: "A placeholder", text: .constant(""), footerText: .constant(nil), isError: .constant(false))
RoundedBorderTextField(placeHolder: "A placeholder", text: .constant("Some text"), footerText: .constant(nil), isError: .constant(false))
RoundedBorderTextField(title: "A title", placeHolder: "A placeholder", text: .constant("Some very long text used to check overlapping with the delete button"), footerText: .constant("Some error text"), isError: .constant(true))
RoundedBorderTextField(title: "A title", placeHolder: "A placeholder", text: .constant("Some very long text used to check overlapping with the delete button"), footerText: .constant("Some normal text"), isError: .constant(false))
}.theme(.dark).preferredColorScheme(.dark)
}
.padding()
}
}
@@ -0,0 +1,81 @@
//
// 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 SearchBar: View {
// MARK: - Properties
var placeholder: String
@Binding var text: String
// MARK: - Private
@Environment(\.theme) private var theme: ThemeSwiftUI
@State private var isEditing = false
var onTextChanged: ((String) -> Void)?
// MARK: - Public
var body: some View {
HStack {
TextField(placeholder, text: $text) { isEditing in
self.isEditing = isEditing
}
.padding(8)
.padding(.horizontal, 25)
.background(theme.colors.navigation)
.cornerRadius(8)
.padding(.leading)
.padding(.trailing, isEditing ? 8 : 16)
.overlay(
HStack {
Image(systemName: "magnifyingglass")
.renderingMode(.template)
.foregroundColor(theme.colors.quarterlyContent)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
if isEditing && !text.isEmpty {
Button(action: {
self.text = ""
}) {
Image(systemName: "multiply.circle.fill")
.renderingMode(.template)
.foregroundColor(theme.colors.quarterlyContent)
}
}
}
.padding(.horizontal, 22)
)
if isEditing {
Button(action: {
self.isEditing = false
self.text = ""
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
}) {
Text(VectorL10n.cancel)
.font(theme.fonts.body)
}
.foregroundColor(theme.colors.accent)
.padding(.trailing)
.transition(.move(edge: .trailing))
}
}
.animation(.default)
}
}
@@ -0,0 +1,82 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
@available(iOS 14.0, *)
struct ThemableButton: View {
// MARK: - Style
private struct Style: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.scaleEffect(configuration.isPressed ? 0.97 : 1)
.animation(.easeOut(duration: 0.2), value: configuration.isPressed)
}
}
// MARK: - Properties
let icon: UIImage?
let title: String
let action: () -> Void
// MARK: Private
@Environment(\.theme) private var theme: ThemeSwiftUI
// MARK: Public
var body: some View {
Button(action: action, label: {
HStack {
Spacer()
if let icon = self.icon {
Image(uiImage: icon).renderingMode(.template).resizable().frame(width: 24, height: 24).foregroundColor(theme.colors.background)
}
Text(title).font(theme.fonts.bodySB).foregroundColor(theme.colors.background)
Spacer()
}
.padding()
.background(theme.colors.accent)
.foregroundColor(theme.colors.background)
.clipShape(RoundedRectangle(cornerRadius: 8))
})
.buttonStyle(Style())
.frame(height: 44)
}
}
// MARK: - Previews
@available(iOS 14.0, *)
struct ThemableButton_Previews: PreviewProvider {
static var previews: some View {
Group {
VStack(alignment: .center, spacing: 20) {
ThemableButton(icon: Asset.Images.spaceTypeIcon.image, title: "A title", action: {}).theme(.light).preferredColorScheme(.light)
ThemableButton(icon: nil, title: "A title", action: {}).theme(.light).preferredColorScheme(.light)
}
VStack(alignment: .center, spacing: 20) {
ThemableButton(icon: Asset.Images.spaceTypeIcon.image, title: "A title", action: {}).theme(.dark).preferredColorScheme(.dark)
ThemableButton(icon: nil, title: "A title", action: {}).theme(.dark).preferredColorScheme(.dark)
}
}
.padding()
}
}
@@ -0,0 +1,87 @@
//
// 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 ThemableNavigationBar: View {
// MARK: - Style
// MARK: - Properties
let title: String?
let showBackButton: Bool
let backAction: () -> Void
let closeAction: () -> Void
// MARK: Private
@Environment(\.theme) private var theme: ThemeSwiftUI
// MARK: Public
@ViewBuilder
var body: some View {
HStack {
Button(action: {backAction()})
{
Image(uiImage: Asset.Images.spacesModalBack.image)
.renderingMode(.template)
.foregroundColor(theme.colors.secondaryContent)
}
.isHidden(!showBackButton)
Spacer()
if let title = title {
Text(title).font(theme.fonts.headline)
.foregroundColor(theme.colors.primaryContent)
}
Spacer()
Button(action: {closeAction()})
{
Image(uiImage: Asset.Images.spacesModalClose.image)
.renderingMode(.template)
.foregroundColor(theme.colors.secondaryContent)
}
}
.padding(.horizontal)
.frame(height: 44)
.background(theme.colors.background)
}
}
// MARK: - Previews
@available(iOS 14.0, *)
struct NavigationBar_Previews: PreviewProvider {
static var previews: some View {
Group {
VStack {
ThemableNavigationBar(title: nil, showBackButton: true, backAction: {}, closeAction: {})
ThemableNavigationBar(title: "Some Title", showBackButton: true, backAction: {}, closeAction: {})
ThemableNavigationBar(title: nil, showBackButton: false, backAction: {}, closeAction: {})
ThemableNavigationBar(title: "Some Title", showBackButton: false, backAction: {}, closeAction: {})
}
VStack {
ThemableNavigationBar(title: nil, showBackButton: true, backAction: {}, closeAction: {}).theme(.dark)
ThemableNavigationBar(title: "Some Title", showBackButton: true, backAction: {}, closeAction: {}).theme(.dark)
ThemableNavigationBar(title: nil, showBackButton: false, backAction: {}, closeAction: {}).theme(.dark)
ThemableNavigationBar(title: "Some Title", showBackButton: false, backAction: {}, closeAction: {}).theme(.dark)
}.preferredColorScheme(.dark)
}
.padding()
}
}
@@ -0,0 +1,162 @@
//
// 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 ThemableTextEditor: UIViewRepresentable {
// MARK: Properties
@Binding var text: String
@State var configuration: UIKitTextInputConfiguration = UIKitTextInputConfiguration()
var onEditingChanged: ((_ edit: Bool) -> Void)?
// MARK: Private
@Environment(\.theme) private var theme: ThemeSwiftUI
private let textView: UITextView = UITextView()
private let internalParams = InternalParams()
// MARK: Setup
init(text: Binding<String>,
configuration: UIKitTextInputConfiguration = UIKitTextInputConfiguration(),
onEditingChanged: ((_ edit: Bool) -> Void)? = nil) {
self._text = text
self._configuration = State(initialValue: configuration)
self.onEditingChanged = onEditingChanged
ResponderManager.register(view: textView)
}
// MARK: UIViewRepresentable
func makeUIView(context: Context) -> UITextView {
textView.delegate = context.coordinator
textView.text = text
if internalParams.isFirstResponder {
textView.becomeFirstResponder()
}
return textView
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.backgroundColor = .clear
uiView.font = UIFont.preferredFont(forTextStyle: .callout)
uiView.textColor = UIColor(theme.colors.primaryContent)
uiView.tintColor = UIColor(theme.colors.accent)
if uiView.text != self.text {
uiView.text = self.text
}
uiView.keyboardType = configuration.keyboardType
uiView.returnKeyType = configuration.returnKeyType
uiView.isSecureTextEntry = configuration.isSecureTextEntry
uiView.autocapitalizationType = configuration.autocapitalizationType
uiView.autocorrectionType = configuration.autocorrectionType
}
static func dismantleUIView(_ uiView: UITextView, coordinator: Coordinator) {
ResponderManager.unregister(view: uiView)
}
// MARK: - Private
private func replaceText(with newText: String) {
self.text = newText
}
private class InternalParams {
var isFirstResponder = false
}
// MARK: - Coordinator
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
class Coordinator: NSObject, UITextViewDelegate {
var parent: ThemableTextEditor
init(_ parent: ThemableTextEditor) {
self.parent = parent
}
func textViewDidBeginEditing(_ textView: UITextView) {
parent.onEditingChanged?(true)
}
func textViewDidEndEditing(_ textView: UITextView) {
parent.onEditingChanged?(false)
}
func textViewDidChange(_ textView: UITextView) {
guard let text = textView.text else {
return
}
parent.replaceText(with: text)
}
@objc func wakeUpNextResponder() {
if !ResponderManager.makeActiveNextResponder(of: parent.textView) {
parent.textView.resignFirstResponder()
}
}
}
}
// MARK: - modifiers
@available(iOS 14.0, *)
extension ThemableTextEditor {
func keyboardType(_ type: UIKeyboardType) -> ThemableTextEditor {
textView.keyboardType = type
return self
}
func isSecureTextEntry(_ isSecure: Bool) -> ThemableTextEditor {
textView.isSecureTextEntry = isSecure
return self
}
func returnKeyType(_ type: UIReturnKeyType) -> ThemableTextEditor {
textView.returnKeyType = type
return self
}
func autocapitalizationType(_ type: UITextAutocapitalizationType) -> ThemableTextEditor {
textView.autocapitalizationType = type
return self
}
func autocorrectionType(_ type: UITextAutocorrectionType) -> ThemableTextEditor {
textView.autocorrectionType = type
return self
}
func makeFirstResponder() -> ThemableTextEditor {
internalParams.isFirstResponder = true
return self
}
}
@@ -0,0 +1,162 @@
//
// 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
struct UIKitTextInputConfiguration {
var keyboardType: UIKeyboardType = .default
var returnKeyType: UIReturnKeyType = .default
var isSecureTextEntry: Bool = false
var autocapitalizationType: UITextAutocapitalizationType = .sentences
var autocorrectionType: UITextAutocorrectionType = .default
}
@available(iOS 14.0, *)
struct ThemableTextField: UIViewRepresentable {
// MARK: Properties
@State var placeholder: String?
@Binding var text: String
@State var configuration: UIKitTextInputConfiguration = UIKitTextInputConfiguration()
var onEditingChanged: ((_ edit: Bool) -> Void)?
var onCommit: (() -> Void)?
// MARK: Private
@Environment(\.theme) private var theme: ThemeSwiftUI
private let textField: UITextField = UITextField()
private let internalParams = InternalParams()
// MARK: Setup
init(placeholder: String? = nil,
text: Binding<String>,
configuration: UIKitTextInputConfiguration = UIKitTextInputConfiguration(),
onEditingChanged: ((_ edit: Bool) -> Void)? = nil,
onCommit: (() -> Void)? = nil) {
self._text = text
self._placeholder = State(initialValue: placeholder)
self._configuration = State(initialValue: configuration)
self.onEditingChanged = onEditingChanged
self.onCommit = onCommit
ResponderManager.register(view: textField)
}
// MARK: UIViewRepresentable
func makeUIView(context: Context) -> UITextField {
textField.delegate = context.coordinator
textField.setContentHuggingPriority(.defaultHigh, for: .vertical)
textField.setContentHuggingPriority(.defaultLow, for: .horizontal)
textField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
textField.text = text
textField.addTarget(context.coordinator, action: #selector(Coordinator.textFieldEditingChanged(sender:)), for: .editingChanged)
if internalParams.isFirstResponder {
textField.becomeFirstResponder()
}
return textField
}
func updateUIView(_ uiView: UITextField, context: Context) {
uiView.backgroundColor = .clear
uiView.font = UIFont.preferredFont(forTextStyle: .callout)
uiView.textColor = UIColor(theme.colors.primaryContent)
uiView.tintColor = UIColor(theme.colors.accent)
if uiView.text != self.text {
uiView.text = self.text
}
uiView.placeholder = placeholder
uiView.keyboardType = configuration.keyboardType
uiView.returnKeyType = configuration.returnKeyType
uiView.isSecureTextEntry = configuration.isSecureTextEntry
uiView.autocapitalizationType = configuration.autocapitalizationType
uiView.autocorrectionType = configuration.autocorrectionType
}
static func dismantleUIView(_ uiView: UITextField, coordinator: Coordinator) {
ResponderManager.unregister(view: uiView)
}
// MARK: - Private
private func replaceText(with newText: String) {
self.text = newText
}
// MARK: - Coordinator
func makeCoordinator() -> Coordinator {
return Coordinator(self)
}
class Coordinator: NSObject, UITextFieldDelegate {
var parent: ThemableTextField
init(_ parent: ThemableTextField) {
self.parent = parent
}
func textFieldDidBeginEditing(_ textField: UITextField) {
parent.onEditingChanged?(true)
}
func textFieldDidEndEditing(_ textField: UITextField) {
parent.onEditingChanged?(false)
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
if parent.configuration.returnKeyType != .next || !ResponderManager.makeActiveNextResponder(of: textField) {
textField.resignFirstResponder()
}
parent.onCommit?()
return true
}
@objc func textFieldEditingChanged(sender: UITextField) {
parent.replaceText(with: sender.text ?? "")
}
}
private class InternalParams {
var isFirstResponder = false
}
}
// MARK: - modifiers
@available(iOS 14.0, *)
extension ThemableTextField {
func makeFirstResponder() -> ThemableTextField {
return makeFirstResponder(true)
}
func makeFirstResponder(_ isFirstResponder: Bool) -> ThemableTextField {
internalParams.isFirstResponder = isFirstResponder
return self
}
}
@@ -0,0 +1,28 @@
//
// 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 13.0, *)
extension View {
@ViewBuilder func isHidden(_ isHidden: Bool) -> some View {
if isHidden {
self.hidden()
} else {
self
}
}
}
@@ -0,0 +1,81 @@
//
// 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
/// `WaitOverlay` allows to easily add an overlay that covers the entire with an `ActivityIndicator` at the center
@available(iOS 14.0, *)
struct WaitOverlay: ViewModifier {
// MARK: - Properties
var alignment: Alignment = .center
@Binding var isLoading: Bool
// MARK: - Private
@Environment(\.theme) private var theme: ThemeSwiftUI
// MARK: - Public
public func body(content: Content) -> some View
{
ZStack(alignment: alignment) {
content
if isLoading {
ZStack {
Color.clear
ActivityIndicator()
}
.frame(width: .infinity, height: .infinity)
.transition(.opacity)
.allowsHitTesting(true)
}
}
.animation(.easeIn(duration: 0.2))
}
}
@available(iOS 14.0, *)
struct WaitView_Previews: PreviewProvider {
static var previews: some View {
Group {
VStack {
ThemableNavigationBar(title: nil, showBackButton: true, backAction: {}, closeAction: {})
ThemableNavigationBar(title: "Some Title", showBackButton: true, backAction: {}, closeAction: {})
ThemableNavigationBar(title: nil, showBackButton: false, backAction: {}, closeAction: {})
ThemableNavigationBar(title: "Some Title", showBackButton: false, backAction: {}, closeAction: {})
}
.modifier(WaitOverlay(isLoading: .constant(true)))
VStack {
ThemableNavigationBar(title: nil, showBackButton: true, backAction: {}, closeAction: {})
ThemableNavigationBar(title: "Some Title", showBackButton: true, backAction: {}, closeAction: {})
ThemableNavigationBar(title: nil, showBackButton: false, backAction: {}, closeAction: {})
ThemableNavigationBar(title: "Some Title", showBackButton: false, backAction: {}, closeAction: {})
}
.modifier(WaitOverlay(alignment:.topLeading, isLoading: .constant(true)))
VStack {
ThemableNavigationBar(title: nil, showBackButton: true, backAction: {}, closeAction: {}).theme(.dark)
ThemableNavigationBar(title: "Some Title", showBackButton: true, backAction: {}, closeAction: {}).theme(.dark)
ThemableNavigationBar(title: nil, showBackButton: false, backAction: {}, closeAction: {}).theme(.dark)
ThemableNavigationBar(title: "Some Title", showBackButton: false, backAction: {}, closeAction: {}).theme(.dark)
}
.modifier(WaitOverlay(isLoading: .constant(true))).theme(.dark)
.preferredColorScheme(.dark)
}
.padding()
}
}
@@ -0,0 +1,275 @@
// File created from TemplateAdvancedRoomsExample
// $ createSwiftUITwoScreen.sh Spaces/SpaceCreation SpaceCreation SpaceCreationMenu SpaceCreationSettings
// File created from FlowTemplate
// $ createRootCoordinator.sh SpaceCreationCoordinator SpaceCreation
/*
Copyright 2021 New Vector Ltd
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import UIKit
@objcMembers
final class SpaceCreationCoordinator: Coordinator {
// MARK: - Properties
// MARK: Private
private let parameters: SpaceCreationCoordinatorParameters
private var navigationRouter: NavigationRouterType {
return self.parameters.navigationRouter
}
private let spaceVisibilityMenuParameters: SpaceCreationMenuCoordinatorParameters
private let spaceSharingTypeMenuParameters: SpaceCreationMenuCoordinatorParameters
// MARK: Public
// Must be used only internally
var childCoordinators: [Coordinator] = []
var callback: ((SpaceCreationCoordinatorAction) -> Void)?
// MARK: - Setup
init(parameters: SpaceCreationCoordinatorParameters) {
self.parameters = parameters
self.spaceVisibilityMenuParameters = SpaceCreationMenuCoordinatorParameters(
session: parameters.session,
creationParams: parameters.creationParameters,
navTitle: VectorL10n.spacesCreateSpaceTitle,
showBackButton: false,
title: VectorL10n.spacesCreationVisibilityTitle,
detail: VectorL10n.spacesCreationVisibilityMessage,
options: [
SpaceCreationMenuRoomOption(id: .publicSpace, icon: Asset.Images.spaceCreationPublic.image, title: VectorL10n.public, detail: VectorL10n.spacePublicJoinRuleDetail),
SpaceCreationMenuRoomOption(id: .privateSpace, icon: Asset.Images.spaceCreationPrivate.image, title: VectorL10n.private, detail: VectorL10n.spacePrivateJoinRuleDetail)
]
)
self.spaceSharingTypeMenuParameters = SpaceCreationMenuCoordinatorParameters(
session: parameters.session,
creationParams: parameters.creationParameters,
navTitle: nil,
showBackButton: true,
title: VectorL10n.spacesCreationSharingTypeTitle,
detail: VectorL10n.spacesCreationSharingTypeMessage(parameters.creationParameters.name ?? ""),
options: [
SpaceCreationMenuRoomOption(id: .ownedPrivateSpace, icon: Asset.Images.tabPeople.image, title: VectorL10n.spacesCreationSharingTypeJustMeTitle, detail: VectorL10n.spacesCreationSharingTypeJustMeDetail),
SpaceCreationMenuRoomOption(id: .sharedPrivateSpace, icon: Asset.Images.tabGroups.image, title: VectorL10n.spacesCreationSharingTypeMeAndTeammatesTitle, detail: VectorL10n.spacesCreationSharingTypeMeAndTeammatesDetail)
]
)
}
// MARK: - Public
func start() {
if #available(iOS 14.0, *) {
MXLog.debug("[SpaceCreationCoordinator] did start.")
let rootCoordinator = self.createMenuCoordinator(with: spaceVisibilityMenuParameters)
rootCoordinator.start()
self.add(childCoordinator: rootCoordinator)
self.toPresentable().isModalInPresentation = true
if self.navigationRouter.modules.isEmpty == false {
self.navigationRouter.push(rootCoordinator, animated: true, popCompletion: { [weak self] in
self?.remove(childCoordinator: rootCoordinator)
})
} else {
self.navigationRouter.setRootModule(rootCoordinator) { [weak self] in
self?.remove(childCoordinator: rootCoordinator)
}
}
}
}
func toPresentable() -> UIViewController {
return self.navigationRouter.toPresentable()
}
// MARK: - Private
@available(iOS 14.0, *)
func pushScreen(with coordinator: Coordinator & Presentable) {
add(childCoordinator: coordinator)
self.navigationRouter.push(coordinator, animated: true, popCompletion: { [weak self] in
self?.remove(childCoordinator: coordinator)
})
coordinator.start()
}
@available(iOS 14.0, *)
private func createMenuCoordinator(with parameters: SpaceCreationMenuCoordinatorParameters) -> SpaceCreationMenuCoordinator {
let coordinator: SpaceCreationMenuCoordinator = SpaceCreationMenuCoordinator(parameters: parameters)
coordinator.callback = { [weak self] result in
MXLog.debug("[SpaceCreationCoordinator] SpaceCreationMenuCoordinator did complete with result \(result).")
guard let self = self else { return }
switch result {
case .didSelectOption(let optionId):
switch optionId {
case .privateSpace, .publicSpace:
self.pushScreen(with: self.createSettingsCoordinator())
case .ownedPrivateSpace:
self.pushScreen(with: self.createRoomChooserCoordinator())
case .sharedPrivateSpace:
self.pushScreen(with: self.createRoomsCoordinator())
}
case .cancel:
self.cancel()
case .back:
self.back()
}
}
return coordinator
}
@available(iOS 14.0, *)
private func createSettingsCoordinator() -> SpaceCreationSettingsCoordinator {
let coordinator = SpaceCreationSettingsCoordinator(parameters: SpaceCreationSettingsCoordinatorParameters(session: parameters.session, creationParameters: parameters.creationParameters))
coordinator.callback = { [weak self] result in
guard let self = self else { return }
switch result {
case .didSetupParameters:
if self.parameters.creationParameters.isPublic {
self.pushScreen(with: self.createRoomsCoordinator())
} else {
self.pushScreen(with: self.createMenuCoordinator(with: self.spaceSharingTypeMenuParameters))
}
case .cancel:
self.cancel()
case .back:
self.back()
}
}
return coordinator
}
@available(iOS 14.0, *)
private func createRoomsCoordinator() -> SpaceCreationRoomsCoordinator {
let coordinator = SpaceCreationRoomsCoordinator(parameters: SpaceCreationRoomsCoordinatorParameters(session: parameters.session, creationParams: parameters.creationParameters))
coordinator.callback = { [weak self] result in
guard let self = self else { return }
switch result {
case .didSetupRooms:
if self.parameters.creationParameters.isPublic {
self.pushScreen(with: self.createPostProcessCoordinator())
} else if self.parameters.creationParameters.isShared {
self.pushScreen(with: self.createEmailInvitesCoordinator())
} else {
UILog.error("[SpaceCreationCoordinator] createRoomsCoordinator: should be public space or shared private space")
}
case .cancel:
self.cancel()
case .back:
self.back()
}
}
return coordinator
}
@available(iOS 14.0, *)
private func createEmailInvitesCoordinator() -> SpaceCreationEmailInvitesCoordinator {
let coordinator = SpaceCreationEmailInvitesCoordinator(parameters: SpaceCreationEmailInvitesCoordinatorParameters(session: parameters.session, creationParams: parameters.creationParameters))
coordinator.callback = { [weak self] result in
guard let self = self else { return }
switch result {
case .cancel:
self.cancel()
case .back:
self.back()
case .done:
self.pushScreen(with: self.createPostProcessCoordinator())
case .inviteByUsername:
self.pushScreen(with: self.createPeopleChooserCoordinator())
}
}
return coordinator
}
@available(iOS 14.0, *)
private func createPeopleChooserCoordinator() -> SpaceCreationMatrixItemChooserCoordinator {
let coordinator = SpaceCreationMatrixItemChooserCoordinator(parameters: SpaceCreationMatrixItemChooserCoordinatorParameters(session: parameters.session, type: .people, creationParams: parameters.creationParameters))
coordinator.callback = { [weak self] result in
guard let self = self else { return }
switch result {
case .cancel:
self.cancel()
case .back:
self.back()
case .done:
self.pushScreen(with: self.createPostProcessCoordinator())
}
}
return coordinator
}
@available(iOS 14.0, *)
private func createRoomChooserCoordinator() -> SpaceCreationMatrixItemChooserCoordinator {
let coordinator = SpaceCreationMatrixItemChooserCoordinator(parameters: SpaceCreationMatrixItemChooserCoordinatorParameters(session: parameters.session, type: .room, creationParams: parameters.creationParameters))
coordinator.callback = { [weak self] result in
guard let self = self else { return }
switch result {
case .cancel:
self.cancel()
case .back:
self.back()
case .done:
self.pushScreen(with: self.createPostProcessCoordinator())
}
}
return coordinator
}
@available(iOS 14.0, *)
private func createPostProcessCoordinator() -> SpaceCreationPostProcessCoordinator {
let coordinator = SpaceCreationPostProcessCoordinator(parameters: SpaceCreationPostProcessCoordinatorParameters(session: parameters.session, creationParams: parameters.creationParameters))
coordinator.callback = { [weak self] result in
guard let self = self else { return }
switch result {
case .done(let spaceId):
self.callback?(.done(spaceId))
case .cancel:
self.cancel()
}
}
return coordinator
}
private func cancel() {
if parameters.creationParameters.isModified {
UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
let alert = UIAlertController(title: VectorL10n.spacesCreationCancelTitle, message: VectorL10n.spacesCreationCancelMessage, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: VectorL10n.stop, style: .destructive, handler: { action in
self.callback?(.cancel)
}))
alert.addAction(UIAlertAction(title: VectorL10n.continue, style: .cancel, handler: nil))
navigationRouter.present(alert, animated: true)
} else {
self.callback?(.cancel)
}
}
private func back() {
navigationRouter.popModule(animated: true)
}
}
@@ -0,0 +1,22 @@
//
// 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
enum SpaceCreationCoordinatorAction {
case cancel
case done(_ spaceId: String)
}
@@ -0,0 +1,40 @@
// File created from TemplateAdvancedRoomsExample
// $ createSwiftUITwoScreen.sh Spaces/SpaceCreation SpaceCreation SpaceCreationMenu SpaceCreationSettings
// File created from FlowTemplate
// $ createRootCoordinator.sh SpaceCreationCoordinator SpaceCreation
/*
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
/// SpaceCreationCoordinator input parameters
struct SpaceCreationCoordinatorParameters {
/// The Matrix session
let session: MXSession
/// Parameters needed to create the new space
let creationParameters: SpaceCreationParameters = SpaceCreationParameters()
/// The navigation router that manage physical navigation
let navigationRouter: NavigationRouterType
init(session: MXSession,
navigationRouter: NavigationRouterType? = nil) {
self.session = session
self.navigationRouter = navigationRouter ?? NavigationRouter(navigationController: RiotNavigationController())
}
}
@@ -0,0 +1,124 @@
// File created from SimpleUserProfileExample
// $ createScreen.sh Spaces/SpaceCreation/SpaceCreationEmailInvites SpaceCreationEmailInvites
/*
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
import SwiftUI
final class SpaceCreationEmailInvitesCoordinator: Coordinator, Presentable {
// MARK: - Properties
// MARK: Private
private let parameters: SpaceCreationEmailInvitesCoordinatorParameters
private let spaceCreationEmailInvitesHostingController: UIViewController
private var spaceCreationEmailInvitesViewModel: SpaceCreationEmailInvitesViewModelProtocol
// MARK: Public
// Must be used only internally
var childCoordinators: [Coordinator] = []
var callback: ((SpaceCreationEmailInvitesCoordinatorAction) -> Void)?
// MARK: - Setup
@available(iOS 14.0, *)
init(parameters: SpaceCreationEmailInvitesCoordinatorParameters) {
self.parameters = parameters
let service = SpaceCreationEmailInvitesService(session: parameters.session)
let viewModel = SpaceCreationEmailInvitesViewModel(creationParameters: parameters.creationParams, service: service)
let view = SpaceCreationEmailInvites(viewModel: viewModel.context)
.addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager))
spaceCreationEmailInvitesViewModel = viewModel
let hostingController = VectorHostingController(rootView: view)
hostingController.isNavigationBarHidden = true
spaceCreationEmailInvitesHostingController = hostingController
}
// MARK: - Public
func start() {
MXLog.debug("[SpaceCreationEmailInvitesCoordinator] did start.")
spaceCreationEmailInvitesViewModel.completion = { [weak self] result in
MXLog.debug("[SpaceCreationEmailInvitesCoordinator] SpaceCreationEmailInvitesViewModel did complete with result: \(result).")
guard let self = self else { return }
switch result {
case .cancel:
self.callback?(.cancel)
case .back:
self.callback?(.back)
case .done:
self.callback?(.done)
case .inviteByUsername:
self.callback?(.inviteByUsername)
case .needIdentityServiceTerms(let baseUrl, let accessToken):
self.presentIdentityServerTerms(with: baseUrl, accessToken: accessToken)
case .identityServiceFailure(let error):
self.showIdentityServiceFailure(error)
}
}
}
func toPresentable() -> UIViewController {
return self.spaceCreationEmailInvitesHostingController
}
// MARK: - Identity service
private var serviceTermsModalCoordinatorBridgePresenter: ServiceTermsModalCoordinatorBridgePresenter?
private func presentIdentityServerTerms(with baseUrl: String?, accessToken: String?) {
guard let baseUrl = baseUrl, let accessToken = accessToken else {
showIdentityServiceFailure(nil)
return
}
let presenter = ServiceTermsModalCoordinatorBridgePresenter(session: parameters.session, baseUrl: baseUrl, serviceType: MXServiceTypeIdentityService, accessToken: accessToken)
presenter.delegate = self
presenter.present(from: self.toPresentable(), animated: true)
serviceTermsModalCoordinatorBridgePresenter = presenter
}
private func showIdentityServiceFailure(_ error: Error?) {
let alertController = UIAlertController(title: VectorL10n.findYourContactsIdentityServiceError, message: nil, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: MatrixKitL10n.ok, style: .default, handler: nil))
self.toPresentable().present(alertController, animated: true, completion: nil);
}
}
extension SpaceCreationEmailInvitesCoordinator: ServiceTermsModalCoordinatorBridgePresenterDelegate {
func serviceTermsModalCoordinatorBridgePresenterDelegateDidAccept(_ coordinatorBridgePresenter: ServiceTermsModalCoordinatorBridgePresenter) {
coordinatorBridgePresenter.dismiss(animated: true) {
self.serviceTermsModalCoordinatorBridgePresenter = nil;
self.callback?(.done)
}
}
func serviceTermsModalCoordinatorBridgePresenterDelegateDidDecline(_ coordinatorBridgePresenter: ServiceTermsModalCoordinatorBridgePresenter, session: MXSession) {
coordinatorBridgePresenter.dismiss(animated: true) {
self.serviceTermsModalCoordinatorBridgePresenter = nil;
}
}
func serviceTermsModalCoordinatorBridgePresenterDelegateDidClose(_ coordinatorBridgePresenter: ServiceTermsModalCoordinatorBridgePresenter) {
coordinatorBridgePresenter.dismiss(animated: true) {
self.serviceTermsModalCoordinatorBridgePresenter = nil;
}
}
}
@@ -0,0 +1,24 @@
// File created from SimpleUserProfileExample
// $ createScreen.sh Spaces/SpaceCreation/SpaceCreationEmailInvites SpaceCreationEmailInvites
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
struct SpaceCreationEmailInvitesCoordinatorParameters {
let session: MXSession
let creationParams: SpaceCreationParameters
}
@@ -0,0 +1,24 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
enum SpaceCreationEmailInvitesCoordinatorAction {
case done
case cancel
case back
case inviteByUsername
}
@@ -0,0 +1,24 @@
// File created from SimpleUserProfileExample
// $ createScreen.sh Spaces/SpaceCreation/SpaceCreationEmailInvites SpaceCreationEmailInvites
//
// 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
enum SpaceCreationEmailInvitesStateAction {
case updateEmailValidity(_ validity: [Bool])
case updateLoading(_ loading: Bool)
}
@@ -0,0 +1,26 @@
// File created from SimpleUserProfileExample
// $ createScreen.sh Spaces/SpaceCreation/SpaceCreationEmailInvites SpaceCreationEmailInvites
//
// 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
enum SpaceCreationEmailInvitesViewAction {
case cancel
case back
case done
case inviteByUsername
}
@@ -0,0 +1,21 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
struct SpaceCreationEmailInvitesViewModelBindings {
var emailInvites: [String]
}
@@ -0,0 +1,28 @@
// File created from SimpleUserProfileExample
// $ createScreen.sh Spaces/SpaceCreation/SpaceCreationEmailInvites SpaceCreationEmailInvites
//
// 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
enum SpaceCreationEmailInvitesViewModelResult {
case cancel
case back
case done
case needIdentityServiceTerms(_ baseUrl: String?, _ accessToken: String?)
case identityServiceFailure(_ error: Error?)
case inviteByUsername
}
@@ -0,0 +1,26 @@
// File created from SimpleUserProfileExample
// $ createScreen.sh Spaces/SpaceCreation/SpaceCreationEmailInvites SpaceCreationEmailInvites
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
struct SpaceCreationEmailInvitesViewState: BindableState {
var title: String
var emailAddressesValid: [Bool]
var loading: Bool
var bindings: SpaceCreationEmailInvitesViewModelBindings
}
@@ -0,0 +1,54 @@
// File created from SimpleUserProfileExample
// $ createScreen.sh Spaces/SpaceCreation/SpaceCreationEmailInvites SpaceCreationEmailInvites
//
// 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 Combine
@available(iOS 14.0, *)
class SpaceCreationEmailInvitesService: SpaceCreationEmailInvitesServiceProtocol {
private let session: MXSession
private(set) var isLoadingSubject: CurrentValueSubject<Bool, Never>
var isIdentityServiceReady: Bool {
if let identityService = session.identityService {
return identityService.areAllTermsAgreed
}
return false
}
init(session: MXSession) {
self.session = session
isLoadingSubject = CurrentValueSubject(false)
}
func validate(_ emailAddresses: [String]) -> [Bool] {
return emailAddresses.map { $0.isEmpty || MXTools.isEmailAddress($0) }
}
func prepareIdentityService(prepared: ((String?, String?) -> Void)?, failure: ((Error?) -> Void)?) {
isLoadingSubject.send(true)
session.prepareIdentityServiceForTerms(withDefault: RiotSettings.shared.identityServerUrlString) { [weak self] session, baseURL, accessToken in
self?.isLoadingSubject.send(false)
prepared?(baseURL, accessToken)
} failure: { [weak self] error in
self?.isLoadingSubject.send(false)
failure?(error)
}
}
}
@@ -0,0 +1,71 @@
// File created from SimpleUserProfileExample
// $ createScreen.sh Spaces/SpaceCreation/SpaceCreationEmailInvites SpaceCreationEmailInvites
//
// 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
/// Using an enum for the screen allows you define the different state cases with
/// the relevant associated data for each case.
@available(iOS 14.0, *)
enum MockSpaceCreationEmailInvitesScreenState: MockScreenState, CaseIterable {
// A case for each state you want to represent
// with specific, minimal associated data that will allow you
// mock that screen.
case defaultEmailValues
case emailEntered
case emailValidationFailed
case loading
/// The associated screen
var screenType: Any.Type {
SpaceCreationEmailInvites.self
}
/// A list of screen state definitions
static var allCases: [MockSpaceCreationEmailInvitesScreenState] {
[.defaultEmailValues, .emailEntered, .emailValidationFailed, .loading]
}
/// Generate the view struct for the screen state.
var screenView: ([Any], AnyView) {
let creationParams = SpaceCreationParameters()
let service: MockSpaceCreationEmailInvitesService
switch self {
case .defaultEmailValues:
service = MockSpaceCreationEmailInvitesService(defaultValidation: true, isLoading: false)
case .emailEntered:
creationParams.emailInvites = ["test1@element.io", "test2@element.io"]
service = MockSpaceCreationEmailInvitesService(defaultValidation: true, isLoading: false)
case .emailValidationFailed:
creationParams.emailInvites = ["test1@element.io", "test2@element.io"]
service = MockSpaceCreationEmailInvitesService(defaultValidation: false, isLoading: false)
case .loading:
creationParams.emailInvites = ["test1@element.io", "test2@element.io"]
service = MockSpaceCreationEmailInvitesService(defaultValidation: true, isLoading: true)
}
let viewModel = SpaceCreationEmailInvitesViewModel(creationParameters: creationParams, service: service)
// can simulate service and viewModel actions here if needs be.
return (
[viewModel],
AnyView(SpaceCreationEmailInvites(viewModel: viewModel.context)
.addDependency(MockAvatarService.example))
)
}
}
@@ -0,0 +1,44 @@
// File created from SimpleUserProfileExample
// $ createScreen.sh Spaces/SpaceCreation/SpaceCreationEmailInvites SpaceCreationEmailInvites
//
// 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 Combine
@available(iOS 14.0, *)
class MockSpaceCreationEmailInvitesService: SpaceCreationEmailInvitesServiceProtocol {
var isLoadingSubject: CurrentValueSubject<Bool, Never>
private let defaultValidation: Bool
var isIdentityServiceReady: Bool {
return true
}
init(defaultValidation: Bool, isLoading: Bool) {
self.defaultValidation = defaultValidation
self.isLoadingSubject = CurrentValueSubject(isLoading)
}
func validate(_ emailAddresses: [String]) -> [Bool] {
return emailAddresses.map { _ in defaultValidation }
}
func prepareIdentityService(prepared: ((String?, String?) -> Void)?, failure: ((Error?) -> Void)?) {
failure?(nil)
}
}
@@ -0,0 +1,28 @@
// File created from SimpleUserProfileExample
// $ createScreen.sh Spaces/SpaceCreation/SpaceCreationEmailInvites SpaceCreationEmailInvites
//
// 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 Combine
@available(iOS 14.0, *)
protocol SpaceCreationEmailInvitesServiceProtocol {
var isIdentityServiceReady: Bool { get }
var isLoadingSubject: CurrentValueSubject<Bool, Never> { get }
func validate(_ emailAddresses: [String]) -> [Bool]
func prepareIdentityService(prepared: ((String?, String?) -> Void)?, failure: ((Error?) -> Void)?)
}
@@ -0,0 +1,51 @@
// File created from SimpleUserProfileExample
// $ createScreen.sh Spaces/SpaceCreation/SpaceCreationEmailInvites SpaceCreationEmailInvites
//
// 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 XCTest
import RiotSwiftUI
@available(iOS 14.0, *)
class SpaceCreationEmailInvitesUITests: MockScreenTest {
override class var screenType: MockScreenState.Type {
return MockSpaceCreationEmailInvitesScreenState.self
}
override class func createTest() -> MockScreenTest {
return SpaceCreationEmailInvitesUITests(selector: #selector(verifySpaceCreationEmailInvitesScreen))
}
func verifySpaceCreationEmailInvitesScreen() throws {
guard let screenState = screenState as? MockSpaceCreationEmailInvitesScreenState else { fatalError("no screen") }
switch screenState {
case .defaultEmailValues:
verifyEmailValues()
case .emailEntered:
verifyEmailValues()
case .emailValidationFailed:
verifyEmailValues()
case .loading:
verifyEmailValues()
}
}
func verifyEmailValues() {
let emailTextFieldsCount = app.textFields.matching(identifier: "emailTextField").count
XCTAssertEqual(emailTextFieldsCount, 2)
}
}
@@ -0,0 +1,41 @@
// File created from SimpleUserProfileExample
// $ createScreen.sh Spaces/SpaceCreation/SpaceCreationEmailInvites SpaceCreationEmailInvites
//
// 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 XCTest
import Combine
@testable import RiotSwiftUI
@available(iOS 14.0, *)
class SpaceCreationEmailInvitesViewModelTests: XCTestCase {
var creationParameters = SpaceCreationParameters()
var service: MockSpaceCreationEmailInvitesService!
var viewModel: SpaceCreationEmailInvitesViewModelProtocol!
var context: SpaceCreationEmailInvitesViewModelType.Context!
override func setUpWithError() throws {
service = MockSpaceCreationEmailInvitesService(defaultValidation: true, isLoading: false)
viewModel = SpaceCreationEmailInvitesViewModel(creationParameters: creationParameters, service: service)
context = viewModel.context
}
func testInitialState() {
XCTAssertEqual(context.viewState.title, creationParameters.isPublic ? VectorL10n.spacesCreationPublicSpaceTitle : VectorL10n.spacesCreationPrivateSpaceTitle)
XCTAssertEqual(context.emailInvites, creationParameters.emailInvites)
}
}
@@ -0,0 +1,124 @@
// File created from SimpleUserProfileExample
// $ createScreen.sh Spaces/SpaceCreation/SpaceCreationEmailInvites SpaceCreationEmailInvites
//
// 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 SpaceCreationEmailInvites: View {
// MARK: - Properties
@ObservedObject var viewModel: SpaceCreationEmailInvitesViewModel.Context
// MARK: - Private
@Environment(\.theme) private var theme: ThemeSwiftUI
// MARK: - Public
@ViewBuilder
var body: some View {
VStack {
ThemableNavigationBar(title: nil, showBackButton: true) {
viewModel.send(viewAction: .back)
} closeAction: {
viewModel.send(viewAction: .cancel)
}
mainView
.animation(.easeInOut(duration: 0.2), value: viewModel.viewState.loading)
.modifier(WaitOverlay(isLoading: .constant(viewModel.viewState.loading)))
}
.background(theme.colors.background)
.navigationBarHidden(true)
}
// MARK: - Private
@ViewBuilder
private var mainView: some View {
VStack {
GeometryReader { reader in
ScrollView {
VStack {
headerView
Spacer()
formView
}
.frame(minHeight: reader.size.height - 2)
}
}
footerView
}
.padding(EdgeInsets(top: 0, leading: 16, bottom: 24, trailing: 16))
}
@ViewBuilder
private var headerView: some View {
VStack {
Text(VectorL10n.spacesCreationEmailInvitesTitle)
.multilineTextAlignment(.center)
.font(theme.fonts.title3SB)
.foregroundColor(theme.colors.primaryContent)
Spacer().frame(height: 20)
Text(VectorL10n.spacesCreationEmailInvitesMessage)
.multilineTextAlignment(.center)
.font(theme.fonts.body)
.foregroundColor(theme.colors.secondaryContent)
}
}
@ViewBuilder
private var formView: some View {
VStack {
VStack(spacing: 20) {
ForEach(viewModel.emailInvites.indices) { index in
RoundedBorderTextField(title: VectorL10n.spacesCreationEmailInvitesEmailTitle, placeHolder: VectorL10n.spacesCreationEmailInvitesEmailTitle, text: $viewModel.emailInvites[index], footerText: .constant(viewModel.viewState.emailAddressesValid[index] ? nil : VectorL10n.authInvalidEmail), isError: .constant(!viewModel.viewState.emailAddressesValid[index]), configuration: UIKitTextInputConfiguration(keyboardType: .emailAddress, returnKeyType: index < viewModel.emailInvites.endIndex - 1 ? .next : .done, autocapitalizationType: .none, autocorrectionType: .no))
.accessibility(identifier: "emailTextField")
}
}
.padding(.horizontal, 2)
.padding(.bottom)
Text(VectorL10n.or)
.font(theme.fonts.caption1)
.foregroundColor(theme.colors.secondaryContent)
.padding(.bottom)
OptionButton(icon: Asset.Images.spacesInviteUsers.image, title: VectorL10n.spacesCreationInviteByUsername, detailMessage: nil) {
viewModel.send(viewAction: .inviteByUsername)
}
.padding(.bottom)
}
}
@ViewBuilder
private var footerView: some View {
ThemableButton(icon: nil, title: VectorL10n.next) {
viewModel.send(viewAction: .done)
}
}
}
// MARK: - Previews
@available(iOS 14.0, *)
struct SpaceCreationEmailInvites_Previews: PreviewProvider {
static let stateRenderer = MockSpaceCreationEmailInvitesScreenState.stateRenderer
static var previews: some View {
stateRenderer.screenGroup(addNavigation: true).theme(.light).preferredColorScheme(.light)
stateRenderer.screenGroup(addNavigation: true).theme(.dark).preferredColorScheme(.dark)
}
}
@@ -0,0 +1,122 @@
// File created from SimpleUserProfileExample
// $ createScreen.sh Spaces/SpaceCreation/SpaceCreationEmailInvites SpaceCreationEmailInvites
//
// 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 Combine
@available(iOS 14, *)
typealias SpaceCreationEmailInvitesViewModelType = StateStoreViewModel<SpaceCreationEmailInvitesViewState,
SpaceCreationEmailInvitesStateAction,
SpaceCreationEmailInvitesViewAction>
@available(iOS 14, *)
class SpaceCreationEmailInvitesViewModel: SpaceCreationEmailInvitesViewModelType, SpaceCreationEmailInvitesViewModelProtocol {
// MARK: - Properties
// MARK: Private
private let creationParameters: SpaceCreationParameters
private let service: SpaceCreationEmailInvitesServiceProtocol
// MARK: Public
var completion: ((SpaceCreationEmailInvitesViewModelResult) -> Void)?
// MARK: - Setup
init(creationParameters: SpaceCreationParameters, service: SpaceCreationEmailInvitesServiceProtocol) {
self.creationParameters = creationParameters
self.service = service
super.init(initialViewState: SpaceCreationEmailInvitesViewModel.defaultState(creationParameters: creationParameters, service: service))
}
private func setupServiceObserving() {
let publisher = service.isLoadingSubject
.map(SpaceCreationEmailInvitesStateAction.updateLoading)
.eraseToAnyPublisher()
dispatch(actionPublisher: publisher)
}
private static func defaultState(creationParameters: SpaceCreationParameters, service: SpaceCreationEmailInvitesServiceProtocol) -> SpaceCreationEmailInvitesViewState {
let emailValidation = service.validate(creationParameters.emailInvites)
let bindings = SpaceCreationEmailInvitesViewModelBindings(emailInvites: creationParameters.emailInvites)
return SpaceCreationEmailInvitesViewState(
title: creationParameters.isPublic ? VectorL10n.spacesCreationPublicSpaceTitle : VectorL10n.spacesCreationPrivateSpaceTitle,
emailAddressesValid: emailValidation,
loading: service.isLoadingSubject.value,
bindings: bindings
)
}
// MARK: - Public
override func process(viewAction: SpaceCreationEmailInvitesViewAction) {
switch viewAction {
case .cancel:
cancel()
case .back:
back()
case .done:
done()
case .inviteByUsername:
inviteByUsername()
}
}
override class func reducer(state: inout SpaceCreationEmailInvitesViewState, action: SpaceCreationEmailInvitesStateAction) {
switch action {
case .updateEmailValidity(let emailValidity):
state.emailAddressesValid = emailValidity
case .updateLoading(let isLoading):
state.loading = isLoading
}
}
private func done() {
self.creationParameters.emailInvites = self.context.emailInvites
self.creationParameters.inviteType = .email
let emailAddressesValidity = service.validate(self.context.emailInvites)
dispatch(action: .updateEmailValidity(emailAddressesValidity))
if self.context.emailInvites.reduce(true, { $0 && $1.isEmpty }) {
completion?(.done)
} else if emailAddressesValidity.reduce(true, { $0 && $1}) {
if service.isIdentityServiceReady {
completion?(.done)
} else {
service.prepareIdentityService { [weak self] baseURL, accessToken in
self?.completion?(.needIdentityServiceTerms(baseURL, accessToken))
} failure: { [weak self] error in
self?.completion?(.identityServiceFailure(error))
}
}
}
}
private func cancel() {
completion?(.cancel)
}
private func back() {
completion?(.back)
}
private func inviteByUsername() {
completion?(.inviteByUsername)
}
}
@@ -0,0 +1,26 @@
// File created from SimpleUserProfileExample
// $ createScreen.sh Spaces/SpaceCreation/SpaceCreationEmailInvites SpaceCreationEmailInvites
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
protocol SpaceCreationEmailInvitesViewModelProtocol {
var completion: ((SpaceCreationEmailInvitesViewModelResult) -> Void)? { get set }
@available(iOS 14, *)
var context: SpaceCreationEmailInvitesViewModelType.Context { get }
}
@@ -0,0 +1,74 @@
// File created from SimpleUserProfileExample
// $ createScreen.sh Spaces/SpaceCreation/SpaceCreationMatrixItemChooser SpaceCreationMatrixItemChooser
/*
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
import SwiftUI
final class SpaceCreationMatrixItemChooserCoordinator: Coordinator, Presentable {
// MARK: - Properties
// MARK: Private
private let parameters: SpaceCreationMatrixItemChooserCoordinatorParameters
private let spaceCreationMatrixItemChooserHostingController: UIViewController
private var spaceCreationMatrixItemChooserViewModel: SpaceCreationMatrixItemChooserViewModelProtocol
// MARK: Public
// Must be used only internally
var childCoordinators: [Coordinator] = []
var callback: ((SpaceCreationMatrixItemChooserCoordinatorAction) -> Void)?
// MARK: - Setup
@available(iOS 14.0, *)
init(parameters: SpaceCreationMatrixItemChooserCoordinatorParameters) {
self.parameters = parameters
let service = SpaceCreationMatrixItemChooserService(session: parameters.session, type: parameters.type, selectedItemIds: [])
let viewModel = SpaceCreationMatrixItemChooserViewModel.makeSpaceCreationMatrixItemChooserViewModel(spaceCreationMatrixItemChooserService: service, creationParams: parameters.creationParams)
let view = SpaceCreationMatrixItemChooser(viewModel: viewModel.context)
.addDependency(AvatarService.instantiate(mediaManager: parameters.session.mediaManager))
spaceCreationMatrixItemChooserViewModel = viewModel
let hostingController = VectorHostingController(rootView: view)
hostingController.isNavigationBarHidden = true
spaceCreationMatrixItemChooserHostingController = hostingController
}
// MARK: - Public
func start() {
MXLog.debug("[SpaceCreationMatrixItemChooserCoordinator] did start.")
spaceCreationMatrixItemChooserViewModel.callback = { [weak self] result in
MXLog.debug("[SpaceCreationMatrixItemChooserCoordinator] SpaceCreationMatrixItemChooserViewModel did complete with result: \(result).")
guard let self = self else { return }
switch result {
case .cancel:
self.callback?(.cancel)
case .back:
self.callback?(.back)
case .done:
self.callback?(.done)
}
}
}
func toPresentable() -> UIViewController {
return self.spaceCreationMatrixItemChooserHostingController
}
}
@@ -0,0 +1,25 @@
// File created from SimpleUserProfileExample
// $ createScreen.sh Spaces/SpaceCreation/SpaceCreationMatrixItemChooser SpaceCreationMatrixItemChooser
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
struct SpaceCreationMatrixItemChooserCoordinatorParameters {
let session: MXSession
let type: SpaceCreationMatrixItemType
let creationParams: SpaceCreationParameters
}
@@ -0,0 +1,26 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
struct SpaceCreationMatrixItem {
let id: String
let avatar: AvatarInput
let displayName: String?
let detailText: String?
}
extension SpaceCreationMatrixItem: Identifiable, Equatable {}
@@ -0,0 +1,24 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
/// Actions returned by the coordinator callback
enum SpaceCreationMatrixItemChooserCoordinatorAction {
case done
case cancel
case back
}
@@ -0,0 +1,23 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
/// Actions to be performed on the `ViewModel` State
enum SpaceCreationMatrixItemListStateAction {
case updateItems([SpaceCreationMatrixItem])
case updateSelection(Set<String>)
}
@@ -0,0 +1,26 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
/// Actions send from the `View` to the `ViewModel`.
enum SpaceCreationMatrixItemListStateActionListViewAction {
case searchTextChanged(String)
case itemTapped(_ itemId: String)
case done
case cancel
case back
}
@@ -0,0 +1,24 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
/// Actions sent by the`ViewModel` to the `Coordinator`.
enum SpaceCreationMatrixItemListStateActionListViewModelAction {
case done
case cancel
case back
}
@@ -0,0 +1,27 @@
//
// Copyright 2021 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
/// State managed by the `ViewModel` delivered to the `View`.
struct SpaceCreationMatrixItemListStateActionListViewState: BindableState {
var navTitle: String
var title: String
var message: String
var emptyListMessage: String
var items: [SpaceCreationMatrixItem]
var selectedItemIds: Set<String>
}

Some files were not shown because too many files have changed in this diff Show More