mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-06 12:28:29 +00:00
Media uploading (#851)
* MediaUploadingPreprocessor - Prevent images without any GPS metadata from being changed * Add support for sending images * Add support for sending video, audio and file attachments * Rename MediaPickerPreview to MediaUploadPreview * Move media uploading to the MediaUploadPreviewScreen, add waiting indicators and error handling * Add support for pasting and drag&dropping in media for upload * Adopt new media picker source UI, remove developer flag * Set minimum heights for timeline loadable images * Fix invalid camera picker file names * Fix flakey MediaUploadingPreprocessor image tests, improve gps metadata stripping * UITests: Update existing screenshots and add new step for the room attachment picker * Switch all github action runners to macos-13 * Cleanup enter key and paste message composer handlers
This commit is contained in:
@@ -9,7 +9,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
integration_tests:
|
integration_tests:
|
||||||
name: Integration Tests
|
name: Integration Tests
|
||||||
runs-on: macos-12
|
runs-on: macos-13
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
# Only allow a single run of this workflow on each branch, automatically cancelling older runs.
|
# Only allow a single run of this workflow on each branch, automatically cancelling older runs.
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ jobs:
|
|||||||
if: contains(github.event.pull_request.labels.*.name, 'Trigger-PR-Build')
|
if: contains(github.event.pull_request.labels.*.name, 'Trigger-PR-Build')
|
||||||
|
|
||||||
name: Release
|
name: Release
|
||||||
runs-on: macos-12
|
runs-on: macos-13
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
# Only allow a single run of this workflow on each branch, automatically cancelling older runs.
|
# Only allow a single run of this workflow on each branch, automatically cancelling older runs.
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
open-translations-pr:
|
open-translations-pr:
|
||||||
runs-on: macos-12
|
runs-on: macos-13
|
||||||
# Skip in forks
|
# Skip in forks
|
||||||
if: github.repository == 'vector-im/element-x-ios'
|
if: github.repository == 'vector-im/element-x-ios'
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
tests:
|
tests:
|
||||||
name: Tests
|
name: Tests
|
||||||
runs-on: macos-12
|
runs-on: macos-13
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
# When running on develop, use the sha to allow all runs of this workflow to run concurrently.
|
# When running on develop, use the sha to allow all runs of this workflow to run concurrently.
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
tests:
|
tests:
|
||||||
name: Tests
|
name: Tests
|
||||||
runs-on: macos-12
|
runs-on: macos-13
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
# When running on develop, use the sha to allow all runs of this workflow to run concurrently.
|
# When running on develop, use the sha to allow all runs of this workflow to run concurrently.
|
||||||
|
|||||||
@@ -138,7 +138,6 @@
|
|||||||
"notification_unread_notified_messages_in_room" = "%1$@ in %2$@";
|
"notification_unread_notified_messages_in_room" = "%1$@ in %2$@";
|
||||||
"notification_unread_notified_messages_in_room_and_invitation" = "%1$@ in %2$@ and %3$@";
|
"notification_unread_notified_messages_in_room_and_invitation" = "%1$@ in %2$@ and %3$@";
|
||||||
"preference_rageshake" = "Rageshake to report bug";
|
"preference_rageshake" = "Rageshake to report bug";
|
||||||
"rageshake_detection_dialog_content" = "You seem to be shaking the phone in frustration. Would you like to open the bug report screen?";
|
|
||||||
"rageshake_dialog_content" = "You seem to be shaking the phone in frustration. Would you like to open the bug report screen?";
|
"rageshake_dialog_content" = "You seem to be shaking the phone in frustration. Would you like to open the bug report screen?";
|
||||||
"report_content_explanation" = "This message will be reported to your homeserver’s administrator. They will not be able to read any encrypted messages.";
|
"report_content_explanation" = "This message will be reported to your homeserver’s administrator. They will not be able to read any encrypted messages.";
|
||||||
"report_content_hint" = "Reason for reporting this content";
|
"report_content_hint" = "Reason for reporting this content";
|
||||||
@@ -177,7 +176,6 @@
|
|||||||
"screen_bug_report_include_logs" = "Send logs to help";
|
"screen_bug_report_include_logs" = "Send logs to help";
|
||||||
"screen_bug_report_include_screenshot" = "Send screenshot";
|
"screen_bug_report_include_screenshot" = "Send screenshot";
|
||||||
"screen_bug_report_logs_description" = "To check things work as intended, logs will be sent with your message. These will be private. To just send your message, turn off this setting.";
|
"screen_bug_report_logs_description" = "To check things work as intended, logs will be sent with your message. These will be private. To just send your message, turn off this setting.";
|
||||||
"screen_bug_report_rash_logs_alert_title" = "%1$@ crashed the last time it was used. Would you like to share a crash report with us?";
|
|
||||||
"screen_change_server_error_invalid_homeserver" = "We couldn't reach this homeserver. Please check that you have entered the homeserver URL correctly. If the URL is correct, contact your homeserver administrator for further help.";
|
"screen_change_server_error_invalid_homeserver" = "We couldn't reach this homeserver. Please check that you have entered the homeserver URL correctly. If the URL is correct, contact your homeserver administrator for further help.";
|
||||||
"screen_change_server_error_no_sliding_sync_message" = "This server currently doesn’t support sliding sync.";
|
"screen_change_server_error_no_sliding_sync_message" = "This server currently doesn’t support sliding sync.";
|
||||||
"screen_change_server_form_header" = "Homeserver URL";
|
"screen_change_server_form_header" = "Homeserver URL";
|
||||||
@@ -193,7 +191,6 @@
|
|||||||
"screen_create_room_public_option_title" = "Public room (anyone)";
|
"screen_create_room_public_option_title" = "Public room (anyone)";
|
||||||
"screen_create_room_room_name_label" = "Room name";
|
"screen_create_room_room_name_label" = "Room name";
|
||||||
"screen_create_room_room_name_placeholder" = "e.g. Product Sprint";
|
"screen_create_room_room_name_placeholder" = "e.g. Product Sprint";
|
||||||
"screen_create_room_title" = "Create a room";
|
|
||||||
"screen_create_room_topic_label" = "Topic (optional)";
|
"screen_create_room_topic_label" = "Topic (optional)";
|
||||||
"screen_create_room_topic_placeholder" = "What is this room about?";
|
"screen_create_room_topic_placeholder" = "What is this room about?";
|
||||||
"screen_invites_decline_chat_message" = "Are you sure you want to decline joining %1$@?";
|
"screen_invites_decline_chat_message" = "Are you sure you want to decline joining %1$@?";
|
||||||
@@ -214,9 +211,13 @@
|
|||||||
"screen_onboarding_welcome_subtitle" = "Welcome to the %1$@ Beta. Supercharged, for speed and simplicity.";
|
"screen_onboarding_welcome_subtitle" = "Welcome to the %1$@ Beta. Supercharged, for speed and simplicity.";
|
||||||
"screen_onboarding_welcome_title" = "Be in your Element";
|
"screen_onboarding_welcome_title" = "Be in your Element";
|
||||||
"screen_report_content_block_user_hint" = "Check if you want to hide all current and future messages from this user";
|
"screen_report_content_block_user_hint" = "Check if you want to hide all current and future messages from this user";
|
||||||
|
"screen_room_attachment_source_camera" = "Camera";
|
||||||
|
"screen_room_attachment_source_camera_photo" = "Take photo";
|
||||||
|
"screen_room_attachment_source_camera_video" = "Record a video";
|
||||||
|
"screen_room_attachment_source_files" = "Attachment";
|
||||||
|
"screen_room_attachment_source_gallery" = "Photo & Video Library";
|
||||||
"screen_room_details_encryption_enabled_subtitle" = "Messages are secured with locks. Only you and the recipients have the unique keys to unlock them.";
|
"screen_room_details_encryption_enabled_subtitle" = "Messages are secured with locks. Only you and the recipients have the unique keys to unlock them.";
|
||||||
"screen_room_details_encryption_enabled_title" = "Message encryption enabled";
|
"screen_room_details_encryption_enabled_title" = "Message encryption enabled";
|
||||||
"screen_room_details_invite_people_title" = "Invite people";
|
|
||||||
"screen_room_details_share_room_title" = "Share room";
|
"screen_room_details_share_room_title" = "Share room";
|
||||||
"screen_room_member_details_block_alert_action" = "Block";
|
"screen_room_member_details_block_alert_action" = "Block";
|
||||||
"screen_room_member_details_block_alert_description" = "Blocked users will not be able to send you messages and all message by them will be hidden. You can reverse this action anytime.";
|
"screen_room_member_details_block_alert_description" = "Blocked users will not be able to send you messages and all message by them will be hidden. You can reverse this action anytime.";
|
||||||
@@ -241,10 +242,8 @@
|
|||||||
"screen_session_verification_waiting_to_accept_subtitle" = "Accept the request to start the verification process in your other session to continue.";
|
"screen_session_verification_waiting_to_accept_subtitle" = "Accept the request to start the verification process in your other session to continue.";
|
||||||
"screen_session_verification_waiting_to_accept_title" = "Waiting to accept request";
|
"screen_session_verification_waiting_to_accept_title" = "Waiting to accept request";
|
||||||
"screen_signout_confirmation_dialog_content" = "Are you sure you want to sign out?";
|
"screen_signout_confirmation_dialog_content" = "Are you sure you want to sign out?";
|
||||||
"screen_signout_confirmation_dialog_submit" = "Sign out";
|
|
||||||
"screen_signout_confirmation_dialog_title" = "Sign out";
|
"screen_signout_confirmation_dialog_title" = "Sign out";
|
||||||
"screen_signout_in_progress_dialog_content" = "Signing out…";
|
"screen_signout_in_progress_dialog_content" = "Signing out…";
|
||||||
"screen_signout_preference_item" = "Sign out";
|
|
||||||
"screen_start_chat_error_starting_chat" = "An error occurred when trying to start a chat";
|
"screen_start_chat_error_starting_chat" = "An error occurred when trying to start a chat";
|
||||||
"screen_start_chat_unknown_profile" = "We can’t validate this user’s Matrix ID. The invite might not be received.";
|
"screen_start_chat_unknown_profile" = "We can’t validate this user’s Matrix ID. The invite might not be received.";
|
||||||
"session_verification_banner_message" = "Looks like you’re using a new device. Verify it’s you to access your encrypted messages.";
|
"session_verification_banner_message" = "Looks like you’re using a new device. Verify it’s you to access your encrypted messages.";
|
||||||
@@ -312,11 +311,14 @@
|
|||||||
"dialog_title_error" = "Error";
|
"dialog_title_error" = "Error";
|
||||||
"dialog_title_success" = "Success";
|
"dialog_title_success" = "Success";
|
||||||
"notification_room_action_quick_reply" = "Quick reply";
|
"notification_room_action_quick_reply" = "Quick reply";
|
||||||
|
"rageshake_detection_dialog_content" = "You seem to be shaking the phone in frustration. Would you like to open the bug report screen?";
|
||||||
"screen_analytics_settings_help_us_improve" = "Help us identify issues and improve %1$@ by sharing anonymous usage data.";
|
"screen_analytics_settings_help_us_improve" = "Help us identify issues and improve %1$@ by sharing anonymous usage data.";
|
||||||
"screen_analytics_settings_read_terms" = "You can read all our terms %1$@.";
|
"screen_analytics_settings_read_terms" = "You can read all our terms %1$@.";
|
||||||
"screen_analytics_settings_read_terms_content_link" = "here";
|
"screen_analytics_settings_read_terms_content_link" = "here";
|
||||||
|
"screen_bug_report_rash_logs_alert_title" = "%1$@ crashed the last time it was used. Would you like to share a crash report with us?";
|
||||||
"screen_change_server_submit" = "Continue";
|
"screen_change_server_submit" = "Continue";
|
||||||
"screen_change_server_title" = "Select your server";
|
"screen_change_server_title" = "Select your server";
|
||||||
|
"screen_create_room_title" = "Create a room";
|
||||||
"screen_dm_details_block_alert_action" = "Block";
|
"screen_dm_details_block_alert_action" = "Block";
|
||||||
"screen_dm_details_block_alert_description" = "Blocked users will not be able to send you messages and all message by them will be hidden. You can reverse this action anytime.";
|
"screen_dm_details_block_alert_description" = "Blocked users will not be able to send you messages and all message by them will be hidden. You can reverse this action anytime.";
|
||||||
"screen_dm_details_block_user" = "Block user";
|
"screen_dm_details_block_user" = "Block user";
|
||||||
@@ -328,9 +330,13 @@
|
|||||||
"screen_login_submit" = "Continue";
|
"screen_login_submit" = "Continue";
|
||||||
"screen_login_username_hint" = "Username";
|
"screen_login_username_hint" = "Username";
|
||||||
"screen_report_content_block_user" = "Block user";
|
"screen_report_content_block_user" = "Block user";
|
||||||
|
"screen_room_details_invite_people_title" = "Invite people";
|
||||||
"screen_room_details_leave_room_title" = "Leave room";
|
"screen_room_details_leave_room_title" = "Leave room";
|
||||||
"screen_room_details_people_title" = "People";
|
"screen_room_details_people_title" = "People";
|
||||||
"screen_room_details_security_title" = "Security";
|
"screen_room_details_security_title" = "Security";
|
||||||
"screen_room_details_topic_title" = "Topic";
|
"screen_room_details_topic_title" = "Topic";
|
||||||
|
"screen_room_error_failed_processing_media" = "Failed processing media to upload, please try again.";
|
||||||
"screen_session_verification_cancelled_title" = "Verification cancelled";
|
"screen_session_verification_cancelled_title" = "Verification cancelled";
|
||||||
"screen_session_verification_positive_button_ready" = "Start";
|
"screen_session_verification_positive_button_ready" = "Start";
|
||||||
|
"screen_signout_confirmation_dialog_submit" = "Sign out";
|
||||||
|
"screen_signout_preference_item" = "Sign out";
|
||||||
|
|||||||
@@ -15,11 +15,3 @@
|
|||||||
"soft_logout_clear_data_submit" = "Clear all data";
|
"soft_logout_clear_data_submit" = "Clear all data";
|
||||||
"soft_logout_clear_data_dialog_title" = "Clear data";
|
"soft_logout_clear_data_dialog_title" = "Clear data";
|
||||||
"soft_logout_clear_data_dialog_content" = "Clear all data currently stored on this device?\nSign in again to access your account data and messages.";
|
"soft_logout_clear_data_dialog_content" = "Clear all data currently stored on this device?\nSign in again to access your account data and messages.";
|
||||||
|
|
||||||
// MARK: - Media upload
|
|
||||||
|
|
||||||
"media_upload_camera_picker" = "Camera";
|
|
||||||
|
|
||||||
"media_upload_photo_and_video_picker" = "Photo & Video Library";
|
|
||||||
|
|
||||||
"media_upload_document_picker" = "Document";
|
|
||||||
|
|||||||
@@ -339,7 +339,7 @@ class AppCoordinator: AppCoordinatorProtocol {
|
|||||||
|
|
||||||
// MARK: Toasts and loading indicators
|
// MARK: Toasts and loading indicators
|
||||||
|
|
||||||
static let loadingIndicatorIdentifier = "AppCoordinatorLoading"
|
private static let loadingIndicatorIdentifier = "AppCoordinatorLoading"
|
||||||
|
|
||||||
private func showLoadingIndicator() {
|
private func showLoadingIndicator() {
|
||||||
ServiceLocator.shared.userIndicatorController.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier,
|
ServiceLocator.shared.userIndicatorController.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier,
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ final class AppSettings {
|
|||||||
case shouldCollapseRoomStateEvents
|
case shouldCollapseRoomStateEvents
|
||||||
case startChatFlowEnabled
|
case startChatFlowEnabled
|
||||||
case startChatUserSuggestionsEnabled
|
case startChatUserSuggestionsEnabled
|
||||||
case mediaUploadingFlowEnabled
|
|
||||||
case invitesFlowEnabled
|
case invitesFlowEnabled
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,11 +166,6 @@ final class AppSettings {
|
|||||||
@UserPreference(key: UserDefaultsKeys.startChatUserSuggestionsEnabled, defaultValue: false, storageType: .volatile)
|
@UserPreference(key: UserDefaultsKeys.startChatUserSuggestionsEnabled, defaultValue: false, storageType: .volatile)
|
||||||
var startChatUserSuggestionsEnabled
|
var startChatUserSuggestionsEnabled
|
||||||
|
|
||||||
// MARK: Media Uploading
|
|
||||||
|
|
||||||
@UserPreference(key: UserDefaultsKeys.mediaUploadingFlowEnabled, defaultValue: false, storageType: .volatile)
|
|
||||||
var mediaUploadingFlowEnabled
|
|
||||||
|
|
||||||
// MARK: Invites
|
// MARK: Invites
|
||||||
|
|
||||||
@UserPreference(key: UserDefaultsKeys.invitesFlowEnabled, defaultValue: false, storageType: .userDefaults(store))
|
@UserPreference(key: UserDefaultsKeys.invitesFlowEnabled, defaultValue: false, storageType: .userDefaults(store))
|
||||||
|
|||||||
@@ -10,12 +10,6 @@ import Foundation
|
|||||||
// swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length
|
// swiftlint:disable explicit_type_interface function_parameter_count identifier_name line_length
|
||||||
// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces
|
// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces
|
||||||
public enum UntranslatedL10n {
|
public enum UntranslatedL10n {
|
||||||
/// Camera
|
|
||||||
public static var mediaUploadCameraPicker: String { return UntranslatedL10n.tr("Untranslated", "media_upload_camera_picker") }
|
|
||||||
/// Document
|
|
||||||
public static var mediaUploadDocumentPicker: String { return UntranslatedL10n.tr("Untranslated", "media_upload_document_picker") }
|
|
||||||
/// Photo & Video Library
|
|
||||||
public static var mediaUploadPhotoAndVideoPicker: String { return UntranslatedL10n.tr("Untranslated", "media_upload_photo_and_video_picker") }
|
|
||||||
/// Clear all data currently stored on this device?
|
/// Clear all data currently stored on this device?
|
||||||
/// Sign in again to access your account data and messages.
|
/// Sign in again to access your account data and messages.
|
||||||
public static var softLogoutClearDataDialogContent: String { return UntranslatedL10n.tr("Untranslated", "soft_logout_clear_data_dialog_content") }
|
public static var softLogoutClearDataDialogContent: String { return UntranslatedL10n.tr("Untranslated", "soft_logout_clear_data_dialog_content") }
|
||||||
|
|||||||
@@ -556,6 +556,16 @@ public enum L10n {
|
|||||||
public static var screenReportContentBlockUser: String { return L10n.tr("Localizable", "screen_report_content_block_user") }
|
public static var screenReportContentBlockUser: String { return L10n.tr("Localizable", "screen_report_content_block_user") }
|
||||||
/// Check if you want to hide all current and future messages from this user
|
/// Check if you want to hide all current and future messages from this user
|
||||||
public static var screenReportContentBlockUserHint: String { return L10n.tr("Localizable", "screen_report_content_block_user_hint") }
|
public static var screenReportContentBlockUserHint: String { return L10n.tr("Localizable", "screen_report_content_block_user_hint") }
|
||||||
|
/// Camera
|
||||||
|
public static var screenRoomAttachmentSourceCamera: String { return L10n.tr("Localizable", "screen_room_attachment_source_camera") }
|
||||||
|
/// Take photo
|
||||||
|
public static var screenRoomAttachmentSourceCameraPhoto: String { return L10n.tr("Localizable", "screen_room_attachment_source_camera_photo") }
|
||||||
|
/// Record a video
|
||||||
|
public static var screenRoomAttachmentSourceCameraVideo: String { return L10n.tr("Localizable", "screen_room_attachment_source_camera_video") }
|
||||||
|
/// Attachment
|
||||||
|
public static var screenRoomAttachmentSourceFiles: String { return L10n.tr("Localizable", "screen_room_attachment_source_files") }
|
||||||
|
/// Photo & Video Library
|
||||||
|
public static var screenRoomAttachmentSourceGallery: String { return L10n.tr("Localizable", "screen_room_attachment_source_gallery") }
|
||||||
/// Messages are secured with locks. Only you and the recipients have the unique keys to unlock them.
|
/// Messages are secured with locks. Only you and the recipients have the unique keys to unlock them.
|
||||||
public static var screenRoomDetailsEncryptionEnabledSubtitle: String { return L10n.tr("Localizable", "screen_room_details_encryption_enabled_subtitle") }
|
public static var screenRoomDetailsEncryptionEnabledSubtitle: String { return L10n.tr("Localizable", "screen_room_details_encryption_enabled_subtitle") }
|
||||||
/// Message encryption enabled
|
/// Message encryption enabled
|
||||||
@@ -572,6 +582,8 @@ public enum L10n {
|
|||||||
public static var screenRoomDetailsShareRoomTitle: String { return L10n.tr("Localizable", "screen_room_details_share_room_title") }
|
public static var screenRoomDetailsShareRoomTitle: String { return L10n.tr("Localizable", "screen_room_details_share_room_title") }
|
||||||
/// Topic
|
/// Topic
|
||||||
public static var screenRoomDetailsTopicTitle: String { return L10n.tr("Localizable", "screen_room_details_topic_title") }
|
public static var screenRoomDetailsTopicTitle: String { return L10n.tr("Localizable", "screen_room_details_topic_title") }
|
||||||
|
/// Failed processing media to upload, please try again.
|
||||||
|
public static var screenRoomErrorFailedProcessingMedia: String { return L10n.tr("Localizable", "screen_room_error_failed_processing_media") }
|
||||||
/// Block
|
/// Block
|
||||||
public static var screenRoomMemberDetailsBlockAlertAction: String { return L10n.tr("Localizable", "screen_room_member_details_block_alert_action") }
|
public static var screenRoomMemberDetailsBlockAlertAction: String { return L10n.tr("Localizable", "screen_room_member_details_block_alert_action") }
|
||||||
/// Blocked users will not be able to send you messages and all message by them will be hidden. You can reverse this action anytime.
|
/// Blocked users will not be able to send you messages and all message by them will be hidden. You can reverse this action anytime.
|
||||||
|
|||||||
@@ -595,6 +595,69 @@ class RoomProxyMock: RoomProxyProtocol {
|
|||||||
return sendImageUrlThumbnailURLImageInfoReturnValue
|
return sendImageUrlThumbnailURLImageInfoReturnValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//MARK: - sendVideo
|
||||||
|
|
||||||
|
var sendVideoUrlThumbnailURLVideoInfoCallsCount = 0
|
||||||
|
var sendVideoUrlThumbnailURLVideoInfoCalled: Bool {
|
||||||
|
return sendVideoUrlThumbnailURLVideoInfoCallsCount > 0
|
||||||
|
}
|
||||||
|
var sendVideoUrlThumbnailURLVideoInfoReceivedArguments: (url: URL, thumbnailURL: URL, videoInfo: VideoInfo)?
|
||||||
|
var sendVideoUrlThumbnailURLVideoInfoReceivedInvocations: [(url: URL, thumbnailURL: URL, videoInfo: VideoInfo)] = []
|
||||||
|
var sendVideoUrlThumbnailURLVideoInfoReturnValue: Result<Void, RoomProxyError>!
|
||||||
|
var sendVideoUrlThumbnailURLVideoInfoClosure: ((URL, URL, VideoInfo) async -> Result<Void, RoomProxyError>)?
|
||||||
|
|
||||||
|
func sendVideo(url: URL, thumbnailURL: URL, videoInfo: VideoInfo) async -> Result<Void, RoomProxyError> {
|
||||||
|
sendVideoUrlThumbnailURLVideoInfoCallsCount += 1
|
||||||
|
sendVideoUrlThumbnailURLVideoInfoReceivedArguments = (url: url, thumbnailURL: thumbnailURL, videoInfo: videoInfo)
|
||||||
|
sendVideoUrlThumbnailURLVideoInfoReceivedInvocations.append((url: url, thumbnailURL: thumbnailURL, videoInfo: videoInfo))
|
||||||
|
if let sendVideoUrlThumbnailURLVideoInfoClosure = sendVideoUrlThumbnailURLVideoInfoClosure {
|
||||||
|
return await sendVideoUrlThumbnailURLVideoInfoClosure(url, thumbnailURL, videoInfo)
|
||||||
|
} else {
|
||||||
|
return sendVideoUrlThumbnailURLVideoInfoReturnValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//MARK: - sendAudio
|
||||||
|
|
||||||
|
var sendAudioUrlAudioInfoCallsCount = 0
|
||||||
|
var sendAudioUrlAudioInfoCalled: Bool {
|
||||||
|
return sendAudioUrlAudioInfoCallsCount > 0
|
||||||
|
}
|
||||||
|
var sendAudioUrlAudioInfoReceivedArguments: (url: URL, audioInfo: AudioInfo)?
|
||||||
|
var sendAudioUrlAudioInfoReceivedInvocations: [(url: URL, audioInfo: AudioInfo)] = []
|
||||||
|
var sendAudioUrlAudioInfoReturnValue: Result<Void, RoomProxyError>!
|
||||||
|
var sendAudioUrlAudioInfoClosure: ((URL, AudioInfo) async -> Result<Void, RoomProxyError>)?
|
||||||
|
|
||||||
|
func sendAudio(url: URL, audioInfo: AudioInfo) async -> Result<Void, RoomProxyError> {
|
||||||
|
sendAudioUrlAudioInfoCallsCount += 1
|
||||||
|
sendAudioUrlAudioInfoReceivedArguments = (url: url, audioInfo: audioInfo)
|
||||||
|
sendAudioUrlAudioInfoReceivedInvocations.append((url: url, audioInfo: audioInfo))
|
||||||
|
if let sendAudioUrlAudioInfoClosure = sendAudioUrlAudioInfoClosure {
|
||||||
|
return await sendAudioUrlAudioInfoClosure(url, audioInfo)
|
||||||
|
} else {
|
||||||
|
return sendAudioUrlAudioInfoReturnValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//MARK: - sendFile
|
||||||
|
|
||||||
|
var sendFileUrlFileInfoCallsCount = 0
|
||||||
|
var sendFileUrlFileInfoCalled: Bool {
|
||||||
|
return sendFileUrlFileInfoCallsCount > 0
|
||||||
|
}
|
||||||
|
var sendFileUrlFileInfoReceivedArguments: (url: URL, fileInfo: FileInfo)?
|
||||||
|
var sendFileUrlFileInfoReceivedInvocations: [(url: URL, fileInfo: FileInfo)] = []
|
||||||
|
var sendFileUrlFileInfoReturnValue: Result<Void, RoomProxyError>!
|
||||||
|
var sendFileUrlFileInfoClosure: ((URL, FileInfo) async -> Result<Void, RoomProxyError>)?
|
||||||
|
|
||||||
|
func sendFile(url: URL, fileInfo: FileInfo) async -> Result<Void, RoomProxyError> {
|
||||||
|
sendFileUrlFileInfoCallsCount += 1
|
||||||
|
sendFileUrlFileInfoReceivedArguments = (url: url, fileInfo: fileInfo)
|
||||||
|
sendFileUrlFileInfoReceivedInvocations.append((url: url, fileInfo: fileInfo))
|
||||||
|
if let sendFileUrlFileInfoClosure = sendFileUrlFileInfoClosure {
|
||||||
|
return await sendFileUrlFileInfoClosure(url, fileInfo)
|
||||||
|
} else {
|
||||||
|
return sendFileUrlFileInfoReturnValue
|
||||||
|
}
|
||||||
|
}
|
||||||
//MARK: - editMessage
|
//MARK: - editMessage
|
||||||
|
|
||||||
var editMessageOriginalCallsCount = 0
|
var editMessageOriginalCallsCount = 0
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ struct A11yIdentifiers {
|
|||||||
struct RoomScreen {
|
struct RoomScreen {
|
||||||
let name = "room-name"
|
let name = "room-name"
|
||||||
let avatar = "room-avatar"
|
let avatar = "room-avatar"
|
||||||
|
let attachmentPicker = "room-attachment_picker"
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RoomDetailsScreen {
|
struct RoomDetailsScreen {
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2023 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 NSItemProvider {
|
||||||
|
var isSupportedForPasteOrDrop: Bool {
|
||||||
|
!registeredContentTypes
|
||||||
|
.compactMap(\.preferredMIMEType)
|
||||||
|
.filter { $0.hasPrefix("image/") || $0.hasPrefix("video/") || $0.hasPrefix("application/") }
|
||||||
|
.isEmpty
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -127,7 +127,7 @@ class AuthenticationCoordinator: CoordinatorProtocol {
|
|||||||
navigationStackCoordinator.push(coordinator)
|
navigationStackCoordinator.push(coordinator)
|
||||||
}
|
}
|
||||||
|
|
||||||
static let loadingIndicatorIdentifier = "AuthenticationCoordinatorLoading"
|
private static let loadingIndicatorIdentifier = "AuthenticationCoordinatorLoading"
|
||||||
|
|
||||||
private func startLoading() {
|
private func startLoading() {
|
||||||
ServiceLocator.shared.userIndicatorController.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier,
|
ServiceLocator.shared.userIndicatorController.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier,
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ final class LoginScreenCoordinator: CoordinatorProtocol {
|
|||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
static let loadingIndicatorIdentifier = "LoginCoordinatorLoading"
|
private static let loadingIndicatorIdentifier = "LoginCoordinatorLoading"
|
||||||
|
|
||||||
private func startLoading(isInteractionBlocking: Bool) {
|
private func startLoading(isInteractionBlocking: Bool) {
|
||||||
if isInteractionBlocking {
|
if isInteractionBlocking {
|
||||||
|
|||||||
+1
-1
@@ -95,7 +95,7 @@ final class SoftLogoutScreenCoordinator: CoordinatorProtocol {
|
|||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
static let loadingIndicatorIdentifier = "SoftLogoutLoading"
|
private static let loadingIndicatorIdentifier = "SoftLogoutLoading"
|
||||||
|
|
||||||
/// Show an activity indicator whilst loading.
|
/// Show an activity indicator whilst loading.
|
||||||
@MainActor private func startLoading() {
|
@MainActor private func startLoading() {
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ final class BugReportScreenCoordinator: CoordinatorProtocol {
|
|||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
static let loadingIndicatorIdentifier = "BugReportLoading"
|
private static let loadingIndicatorIdentifier = "BugReportLoading"
|
||||||
|
|
||||||
private func startLoading(label: String = L10n.commonLoading, progressPublisher: ProgressPublisher) {
|
private func startLoading(label: String = L10n.commonLoading, progressPublisher: ProgressPublisher) {
|
||||||
parameters.userIndicatorController?.submitIndicator(
|
parameters.userIndicatorController?.submitIndicator(
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ struct DeveloperOptionsScreenViewStateBindings {
|
|||||||
var shouldCollapseRoomStateEvents: Bool
|
var shouldCollapseRoomStateEvents: Bool
|
||||||
var startChatFlowEnabled: Bool
|
var startChatFlowEnabled: Bool
|
||||||
var startChatUserSuggestionsEnabled: Bool
|
var startChatUserSuggestionsEnabled: Bool
|
||||||
var mediaUploadFlowEnabled: Bool
|
|
||||||
var invitesFlowEnabled: Bool
|
var invitesFlowEnabled: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,6 +33,5 @@ enum DeveloperOptionsScreenViewAction {
|
|||||||
case changedShouldCollapseRoomStateEvents
|
case changedShouldCollapseRoomStateEvents
|
||||||
case changedStartChatFlowEnabled
|
case changedStartChatFlowEnabled
|
||||||
case changedStartChatUserSuggestionsEnabled
|
case changedStartChatUserSuggestionsEnabled
|
||||||
case changedMediaUploadFlowEnabled
|
|
||||||
case changedInvitesFlowEnabled
|
case changedInvitesFlowEnabled
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ class DeveloperOptionsScreenViewModel: DeveloperOptionsScreenViewModelType, Deve
|
|||||||
let bindings = DeveloperOptionsScreenViewStateBindings(shouldCollapseRoomStateEvents: ServiceLocator.shared.settings.shouldCollapseRoomStateEvents,
|
let bindings = DeveloperOptionsScreenViewStateBindings(shouldCollapseRoomStateEvents: ServiceLocator.shared.settings.shouldCollapseRoomStateEvents,
|
||||||
startChatFlowEnabled: ServiceLocator.shared.settings.startChatFlowEnabled,
|
startChatFlowEnabled: ServiceLocator.shared.settings.startChatFlowEnabled,
|
||||||
startChatUserSuggestionsEnabled: ServiceLocator.shared.settings.startChatUserSuggestionsEnabled,
|
startChatUserSuggestionsEnabled: ServiceLocator.shared.settings.startChatUserSuggestionsEnabled,
|
||||||
mediaUploadFlowEnabled: ServiceLocator.shared.settings.mediaUploadingFlowEnabled,
|
|
||||||
invitesFlowEnabled: ServiceLocator.shared.settings.invitesFlowEnabled)
|
invitesFlowEnabled: ServiceLocator.shared.settings.invitesFlowEnabled)
|
||||||
let state = DeveloperOptionsScreenViewState(bindings: bindings)
|
let state = DeveloperOptionsScreenViewState(bindings: bindings)
|
||||||
|
|
||||||
@@ -44,8 +43,6 @@ class DeveloperOptionsScreenViewModel: DeveloperOptionsScreenViewModelType, Deve
|
|||||||
ServiceLocator.shared.settings.startChatFlowEnabled = state.bindings.startChatFlowEnabled
|
ServiceLocator.shared.settings.startChatFlowEnabled = state.bindings.startChatFlowEnabled
|
||||||
case .changedStartChatUserSuggestionsEnabled:
|
case .changedStartChatUserSuggestionsEnabled:
|
||||||
ServiceLocator.shared.settings.startChatUserSuggestionsEnabled = state.bindings.startChatUserSuggestionsEnabled
|
ServiceLocator.shared.settings.startChatUserSuggestionsEnabled = state.bindings.startChatUserSuggestionsEnabled
|
||||||
case .changedMediaUploadFlowEnabled:
|
|
||||||
ServiceLocator.shared.settings.mediaUploadingFlowEnabled = state.bindings.mediaUploadFlowEnabled
|
|
||||||
case .changedInvitesFlowEnabled:
|
case .changedInvitesFlowEnabled:
|
||||||
ServiceLocator.shared.settings.invitesFlowEnabled = state.bindings.invitesFlowEnabled
|
ServiceLocator.shared.settings.invitesFlowEnabled = state.bindings.invitesFlowEnabled
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,13 +44,6 @@ struct DeveloperOptionsScreen: View {
|
|||||||
context.send(viewAction: .changedStartChatUserSuggestionsEnabled)
|
context.send(viewAction: .changedStartChatUserSuggestionsEnabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
Toggle(isOn: $context.mediaUploadFlowEnabled) {
|
|
||||||
Text("Show Media Uploading flow")
|
|
||||||
}
|
|
||||||
.onChange(of: context.mediaUploadFlowEnabled) { _ in
|
|
||||||
context.send(viewAction: .changedMediaUploadFlowEnabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
Toggle(isOn: $context.invitesFlowEnabled) {
|
Toggle(isOn: $context.invitesFlowEnabled) {
|
||||||
Text("Show Invites flow")
|
Text("Show Invites flow")
|
||||||
}
|
}
|
||||||
|
|||||||
-56
@@ -1,56 +0,0 @@
|
|||||||
//
|
|
||||||
// Copyright 2022 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 MediaPickerPreviewScreenCoordinatorParameters {
|
|
||||||
let url: URL
|
|
||||||
let title: String?
|
|
||||||
}
|
|
||||||
|
|
||||||
enum MediaPickerPreviewScreenCoordinatorAction {
|
|
||||||
case send
|
|
||||||
case cancel
|
|
||||||
}
|
|
||||||
|
|
||||||
final class MediaPickerPreviewScreenCoordinator: CoordinatorProtocol {
|
|
||||||
private let parameters: MediaPickerPreviewScreenCoordinatorParameters
|
|
||||||
|
|
||||||
private var viewModel: MediaPickerPreviewScreenViewModelProtocol
|
|
||||||
private let callback: (MediaPickerPreviewScreenCoordinatorAction) -> Void
|
|
||||||
|
|
||||||
init(parameters: MediaPickerPreviewScreenCoordinatorParameters, callback: @escaping (MediaPickerPreviewScreenCoordinatorAction) -> Void) {
|
|
||||||
self.parameters = parameters
|
|
||||||
self.callback = callback
|
|
||||||
|
|
||||||
viewModel = MediaPickerPreviewScreenViewModel(url: parameters.url, title: parameters.title)
|
|
||||||
}
|
|
||||||
|
|
||||||
func start() {
|
|
||||||
viewModel.callback = { [weak self] action in
|
|
||||||
switch action {
|
|
||||||
case .send:
|
|
||||||
self?.callback(.send)
|
|
||||||
case .cancel:
|
|
||||||
self?.callback(.cancel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func toPresentable() -> AnyView {
|
|
||||||
AnyView(MediaPickerPreviewScreen(context: viewModel.context))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-36
@@ -1,36 +0,0 @@
|
|||||||
//
|
|
||||||
// Copyright 2022 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
|
|
||||||
|
|
||||||
typealias MediaPickerPreviewScreenViewModelType = StateStoreViewModel<MediaPickerPreviewScreenViewState, MediaPickerPreviewScreenViewAction>
|
|
||||||
|
|
||||||
class MediaPickerPreviewScreenViewModel: MediaPickerPreviewScreenViewModelType, MediaPickerPreviewScreenViewModelProtocol {
|
|
||||||
var callback: ((MediaPickerPreviewScreenViewModelAction) -> Void)?
|
|
||||||
|
|
||||||
init(url: URL, title: String?) {
|
|
||||||
super.init(initialViewState: MediaPickerPreviewScreenViewState(url: url, title: title))
|
|
||||||
}
|
|
||||||
|
|
||||||
override func process(viewAction: MediaPickerPreviewScreenViewAction) {
|
|
||||||
switch viewAction {
|
|
||||||
case .send:
|
|
||||||
callback?(.send)
|
|
||||||
case .cancel:
|
|
||||||
callback?(.cancel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -70,7 +70,7 @@ struct CameraPicker: UIViewControllerRepresentable {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let fileName = "\(Date.now.formatted(date: .abbreviated, time: .shortened)).jpg"
|
let fileName = "\(Date.now.formatted(.iso8601.dateSeparator(.omitted).timeSeparator(.omitted))).jpg"
|
||||||
|
|
||||||
do {
|
do {
|
||||||
let url = try FileManager.default.writeDataToTemporaryDirectory(data: jpegData, fileName: fileName)
|
let url = try FileManager.default.writeDataToTemporaryDirectory(data: jpegData, fileName: fileName)
|
||||||
|
|||||||
@@ -25,14 +25,15 @@ enum MediaPickerScreenSource {
|
|||||||
enum MediaPickerScreenCoordinatorAction {
|
enum MediaPickerScreenCoordinatorAction {
|
||||||
case selectMediaAtURL(URL)
|
case selectMediaAtURL(URL)
|
||||||
case cancel
|
case cancel
|
||||||
case error(Error?)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class MediaPickerScreenCoordinator: CoordinatorProtocol {
|
class MediaPickerScreenCoordinator: CoordinatorProtocol {
|
||||||
|
private weak var userIndicatorController: UserIndicatorControllerProtocol?
|
||||||
private let source: MediaPickerScreenSource
|
private let source: MediaPickerScreenSource
|
||||||
private let callback: ((MediaPickerScreenCoordinatorAction) -> Void)?
|
private let callback: ((MediaPickerScreenCoordinatorAction) -> Void)?
|
||||||
|
|
||||||
init(source: MediaPickerScreenSource, callback: @escaping (MediaPickerScreenCoordinatorAction) -> Void) {
|
init(userIndicatorController: UserIndicatorControllerProtocol, source: MediaPickerScreenSource, callback: @escaping (MediaPickerScreenCoordinatorAction) -> Void) {
|
||||||
|
self.userIndicatorController = userIndicatorController
|
||||||
self.source = source
|
self.source = source
|
||||||
self.callback = callback
|
self.callback = callback
|
||||||
}
|
}
|
||||||
@@ -52,7 +53,8 @@ class MediaPickerScreenCoordinator: CoordinatorProtocol {
|
|||||||
case .cancel:
|
case .cancel:
|
||||||
self?.callback?(.cancel)
|
self?.callback?(.cancel)
|
||||||
case .error(let error):
|
case .error(let error):
|
||||||
self?.callback?(.error(error))
|
MXLog.error("Failed selecting media from the photo library with error: \(error)")
|
||||||
|
self?.showError()
|
||||||
case .selectFile(let url):
|
case .selectFile(let url):
|
||||||
self?.callback?(.selectMediaAtURL(url))
|
self?.callback?(.selectMediaAtURL(url))
|
||||||
}
|
}
|
||||||
@@ -65,7 +67,8 @@ class MediaPickerScreenCoordinator: CoordinatorProtocol {
|
|||||||
case .cancel:
|
case .cancel:
|
||||||
self.callback?(.cancel)
|
self.callback?(.cancel)
|
||||||
case .error(let error):
|
case .error(let error):
|
||||||
self.callback?(.error(error))
|
MXLog.error("Failed selecting media from the document picker with error: \(error)")
|
||||||
|
self.showError()
|
||||||
case .selectFile(let url):
|
case .selectFile(let url):
|
||||||
self.callback?(.selectMediaAtURL(url))
|
self.callback?(.selectMediaAtURL(url))
|
||||||
}
|
}
|
||||||
@@ -79,11 +82,16 @@ class MediaPickerScreenCoordinator: CoordinatorProtocol {
|
|||||||
case .cancel:
|
case .cancel:
|
||||||
self?.callback?(.cancel)
|
self?.callback?(.cancel)
|
||||||
case .error(let error):
|
case .error(let error):
|
||||||
self?.callback?(.error(error))
|
MXLog.error("Failed selecting media from the camera picker with error: \(error)")
|
||||||
|
self?.showError()
|
||||||
case .selectFile(let url):
|
case .selectFile(let url):
|
||||||
self?.callback?(.selectMediaAtURL(url))
|
self?.callback?(.selectMediaAtURL(url))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.background(.black, ignoresSafeAreaEdges: .bottom)
|
.background(.black, ignoresSafeAreaEdges: .bottom)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func showError() {
|
||||||
|
userIndicatorController?.submitIndicator(UserIndicator(title: L10n.screenMediaPickerErrorFailedSelection))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,12 @@ import SwiftUI
|
|||||||
enum PhotoLibraryPickerAction {
|
enum PhotoLibraryPickerAction {
|
||||||
case selectFile(URL)
|
case selectFile(URL)
|
||||||
case cancel
|
case cancel
|
||||||
case error(Error?)
|
case error(PhotoLibraryPickerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PhotoLibraryPickerError: Error {
|
||||||
|
case failedLoadingFileRepresentation(Error?)
|
||||||
|
case failedCopyingFile
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PhotoLibraryPicker: UIViewControllerRepresentable {
|
struct PhotoLibraryPicker: UIViewControllerRepresentable {
|
||||||
@@ -63,7 +68,9 @@ struct PhotoLibraryPicker: UIViewControllerRepresentable {
|
|||||||
|
|
||||||
provider.loadFileRepresentation(forTypeIdentifier: "public.item") { [weak self] url, error in
|
provider.loadFileRepresentation(forTypeIdentifier: "public.item") { [weak self] url, error in
|
||||||
guard let url else {
|
guard let url else {
|
||||||
self?.photoLibraryPicker.callback(.error(error))
|
Task { @MainActor in
|
||||||
|
self?.photoLibraryPicker.callback(.error(.failedLoadingFileRepresentation(error)))
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +83,9 @@ struct PhotoLibraryPicker: UIViewControllerRepresentable {
|
|||||||
self?.photoLibraryPicker.callback(.selectFile(newURL))
|
self?.photoLibraryPicker.callback(.selectFile(newURL))
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
self?.photoLibraryPicker.callback(.error(error))
|
Task { @MainActor in
|
||||||
|
self?.photoLibraryPicker.callback(.error(.failedCopyingFile))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+57
@@ -0,0 +1,57 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2022 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 MediaUploadPreviewScreenCoordinatorParameters {
|
||||||
|
weak var userIndicatorController: UserIndicatorControllerProtocol?
|
||||||
|
let roomProxy: RoomProxyProtocol
|
||||||
|
let mediaUploadingPreprocessor: MediaUploadingPreprocessor
|
||||||
|
let title: String?
|
||||||
|
let url: URL
|
||||||
|
}
|
||||||
|
|
||||||
|
enum MediaUploadPreviewScreenCoordinatorAction {
|
||||||
|
case dismiss
|
||||||
|
}
|
||||||
|
|
||||||
|
final class MediaUploadPreviewScreenCoordinator: CoordinatorProtocol {
|
||||||
|
private var viewModel: MediaUploadPreviewScreenViewModelProtocol
|
||||||
|
private let callback: (MediaUploadPreviewScreenCoordinatorAction) -> Void
|
||||||
|
|
||||||
|
init(parameters: MediaUploadPreviewScreenCoordinatorParameters, callback: @escaping (MediaUploadPreviewScreenCoordinatorAction) -> Void) {
|
||||||
|
self.callback = callback
|
||||||
|
|
||||||
|
viewModel = MediaUploadPreviewScreenViewModel(userIndicatorController: parameters.userIndicatorController,
|
||||||
|
roomProxy: parameters.roomProxy,
|
||||||
|
mediaUploadingPreprocessor: parameters.mediaUploadingPreprocessor,
|
||||||
|
title: parameters.title,
|
||||||
|
url: parameters.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
func start() {
|
||||||
|
viewModel.callback = { [weak self] action in
|
||||||
|
switch action {
|
||||||
|
case .dismiss:
|
||||||
|
self?.callback(.dismiss)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toPresentable() -> AnyView {
|
||||||
|
AnyView(MediaUploadPreviewScreen(context: viewModel.context))
|
||||||
|
}
|
||||||
|
}
|
||||||
+4
-5
@@ -16,17 +16,16 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
enum MediaPickerPreviewScreenViewModelAction {
|
enum MediaUploadPreviewScreenViewModelAction {
|
||||||
case send
|
case dismiss
|
||||||
case cancel
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct MediaPickerPreviewScreenViewState: BindableState {
|
struct MediaUploadPreviewScreenViewState: BindableState {
|
||||||
let url: URL
|
let url: URL
|
||||||
let title: String?
|
let title: String?
|
||||||
}
|
}
|
||||||
|
|
||||||
enum MediaPickerPreviewScreenViewAction {
|
enum MediaUploadPreviewScreenViewAction {
|
||||||
case send
|
case send
|
||||||
case cancel
|
case cancel
|
||||||
}
|
}
|
||||||
+105
@@ -0,0 +1,105 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2022 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
|
||||||
|
|
||||||
|
typealias MediaUploadPreviewScreenViewModelType = StateStoreViewModel<MediaUploadPreviewScreenViewState, MediaUploadPreviewScreenViewAction>
|
||||||
|
|
||||||
|
class MediaUploadPreviewScreenViewModel: MediaUploadPreviewScreenViewModelType, MediaUploadPreviewScreenViewModelProtocol {
|
||||||
|
private weak var userIndicatorController: UserIndicatorControllerProtocol?
|
||||||
|
private let roomProxy: RoomProxyProtocol
|
||||||
|
private let mediaUploadingPreprocessor: MediaUploadingPreprocessor
|
||||||
|
private let url: URL
|
||||||
|
|
||||||
|
var callback: ((MediaUploadPreviewScreenViewModelAction) -> Void)?
|
||||||
|
|
||||||
|
init(userIndicatorController: UserIndicatorControllerProtocol?,
|
||||||
|
roomProxy: RoomProxyProtocol,
|
||||||
|
mediaUploadingPreprocessor: MediaUploadingPreprocessor,
|
||||||
|
title: String?,
|
||||||
|
url: URL) {
|
||||||
|
self.userIndicatorController = userIndicatorController
|
||||||
|
self.roomProxy = roomProxy
|
||||||
|
self.mediaUploadingPreprocessor = mediaUploadingPreprocessor
|
||||||
|
self.url = url
|
||||||
|
|
||||||
|
super.init(initialViewState: MediaUploadPreviewScreenViewState(url: url, title: title))
|
||||||
|
}
|
||||||
|
|
||||||
|
override func process(viewAction: MediaUploadPreviewScreenViewAction) {
|
||||||
|
switch viewAction {
|
||||||
|
case .send:
|
||||||
|
Task {
|
||||||
|
startLoading()
|
||||||
|
defer {
|
||||||
|
stopLoading()
|
||||||
|
}
|
||||||
|
|
||||||
|
switch await mediaUploadingPreprocessor.processMedia(at: url) {
|
||||||
|
case .success(let mediaInfo):
|
||||||
|
switch await sendAttachment(mediaInfo: mediaInfo) {
|
||||||
|
case .success:
|
||||||
|
callback?(.dismiss)
|
||||||
|
case .failure(let error):
|
||||||
|
MXLog.error("Failed sending attachment with error: \(error)")
|
||||||
|
showError(label: L10n.screenMediaUploadPreviewErrorFailedSending)
|
||||||
|
}
|
||||||
|
|
||||||
|
case .failure(let error):
|
||||||
|
MXLog.error("Failed processing media to upload with error: \(error)")
|
||||||
|
showError(label: L10n.screenMediaUploadPreviewErrorFailedProcessing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case .cancel:
|
||||||
|
callback?(.dismiss)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Private
|
||||||
|
|
||||||
|
private func sendAttachment(mediaInfo: MediaInfo) async -> Result<Void, RoomProxyError> {
|
||||||
|
switch mediaInfo {
|
||||||
|
case let .image(imageURL, thumbnailURL, imageInfo):
|
||||||
|
return await roomProxy.sendImage(url: imageURL, thumbnailURL: thumbnailURL, imageInfo: imageInfo)
|
||||||
|
case let .video(videoURL, thumbnailURL, videoInfo):
|
||||||
|
return await roomProxy.sendVideo(url: videoURL, thumbnailURL: thumbnailURL, videoInfo: videoInfo)
|
||||||
|
case let .audio(audioURL, audioInfo):
|
||||||
|
return await roomProxy.sendAudio(url: audioURL, audioInfo: audioInfo)
|
||||||
|
case let .file(fileURL, fileInfo):
|
||||||
|
return await roomProxy.sendFile(url: fileURL, fileInfo: fileInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static let loadingIndicatorIdentifier = "MediaUploadPreviewLoading"
|
||||||
|
|
||||||
|
private func startLoading() {
|
||||||
|
userIndicatorController?.submitIndicator(
|
||||||
|
UserIndicator(id: Self.loadingIndicatorIdentifier,
|
||||||
|
type: .modal,
|
||||||
|
title: L10n.commonLoading,
|
||||||
|
persistent: true)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func stopLoading() {
|
||||||
|
userIndicatorController?.retractIndicatorWithId(Self.loadingIndicatorIdentifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showError(label: String) {
|
||||||
|
userIndicatorController?.submitIndicator(UserIndicator(title: label))
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-3
@@ -17,7 +17,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
protocol MediaPickerPreviewScreenViewModelProtocol {
|
protocol MediaUploadPreviewScreenViewModelProtocol {
|
||||||
var callback: ((MediaPickerPreviewScreenViewModelAction) -> Void)? { get set }
|
var callback: ((MediaUploadPreviewScreenViewModelAction) -> Void)? { get set }
|
||||||
var context: MediaPickerPreviewScreenViewModelType.Context { get }
|
var context: MediaUploadPreviewScreenViewModelType.Context { get }
|
||||||
}
|
}
|
||||||
+17
-14
@@ -17,18 +17,17 @@
|
|||||||
import QuickLook
|
import QuickLook
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct MediaPickerPreviewScreen: View {
|
struct MediaUploadPreviewScreen: View {
|
||||||
@ObservedObject var context: MediaPickerPreviewScreenViewModel.Context
|
@ObservedObject var context: MediaUploadPreviewScreenViewModel.Context
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
PreviewView(context: context,
|
||||||
PreviewView(context: context,
|
fileURL: context.viewState.url,
|
||||||
fileURL: context.viewState.url,
|
title: context.viewState.title)
|
||||||
title: context.viewState.title)
|
.id(UUID())
|
||||||
.id(UUID())
|
.ignoresSafeArea(edges: .bottom)
|
||||||
.ignoresSafeArea(edges: .bottom)
|
.toolbar { toolbar }
|
||||||
.toolbar { toolbar }
|
.interactiveDismissDisabled()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ToolbarContentBuilder
|
@ToolbarContentBuilder
|
||||||
@@ -47,7 +46,7 @@ struct MediaPickerPreviewScreen: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private struct PreviewView: UIViewControllerRepresentable {
|
private struct PreviewView: UIViewControllerRepresentable {
|
||||||
let context: MediaPickerPreviewScreenViewModel.Context
|
let context: MediaUploadPreviewScreenViewModel.Context
|
||||||
let fileURL: URL
|
let fileURL: URL
|
||||||
let title: String?
|
let title: String?
|
||||||
|
|
||||||
@@ -102,9 +101,13 @@ private class PreviewItem: NSObject, QLPreviewItem {
|
|||||||
|
|
||||||
// MARK: - Previews
|
// MARK: - Previews
|
||||||
|
|
||||||
struct MediaPickerPreviewScreen_Previews: PreviewProvider {
|
struct MediaUploadPreviewScreen_Previews: PreviewProvider {
|
||||||
static let viewModel = MediaPickerPreviewScreenViewModel(url: URL.picturesDirectory, title: nil)
|
static let viewModel = MediaUploadPreviewScreenViewModel(userIndicatorController: MockUserIndicatorController(),
|
||||||
|
roomProxy: RoomProxyMock(),
|
||||||
|
mediaUploadingPreprocessor: MediaUploadingPreprocessor(),
|
||||||
|
title: nil,
|
||||||
|
url: URL.picturesDirectory)
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
MediaPickerPreviewScreen(context: viewModel.context)
|
MediaUploadPreviewScreen(context: viewModel.context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -68,6 +68,8 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
|||||||
self.displayMediaPickerWithSource(.photoLibrary)
|
self.displayMediaPickerWithSource(.photoLibrary)
|
||||||
case .displayDocumentPicker:
|
case .displayDocumentPicker:
|
||||||
self.displayMediaPickerWithSource(.documents)
|
self.displayMediaPickerWithSource(.documents)
|
||||||
|
case .displayMediaUploadPreviewScreen(let url):
|
||||||
|
self.displayMediaUploadPreviewScreenForFile(at: url)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,44 +86,43 @@ final class RoomScreenCoordinator: CoordinatorProtocol {
|
|||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
private func displayMediaPickerWithSource(_ source: MediaPickerScreenSource) {
|
private func displayMediaPickerWithSource(_ source: MediaPickerScreenSource) {
|
||||||
let mediaPickerCoordinator = MediaPickerScreenCoordinator(source: source) { [weak self] action in
|
let stackCoordinator = NavigationStackCoordinator()
|
||||||
|
let userIndicatorController = UserIndicatorController(rootCoordinator: stackCoordinator)
|
||||||
|
|
||||||
|
let mediaPickerCoordinator = MediaPickerScreenCoordinator(userIndicatorController: userIndicatorController, source: source) { [weak self] action in
|
||||||
switch action {
|
switch action {
|
||||||
case .cancel:
|
case .cancel:
|
||||||
self?.navigationStackCoordinator.setSheetCoordinator(nil)
|
self?.navigationStackCoordinator.setSheetCoordinator(nil)
|
||||||
case .error:
|
|
||||||
break
|
|
||||||
case .selectMediaAtURL(let url):
|
case .selectMediaAtURL(let url):
|
||||||
let mediaPickerPreviewScreenCoordinator = MediaPickerPreviewScreenCoordinator(parameters: .init(url: url, title: url.lastPathComponent)) { action in
|
self?.displayMediaUploadPreviewScreenForFile(at: url)
|
||||||
switch action {
|
|
||||||
case .send:
|
|
||||||
Task {
|
|
||||||
let preprocessor = MediaUploadingPreprocessor()
|
|
||||||
switch await preprocessor.processMedia(at: url) {
|
|
||||||
case .success(let mediaInfo):
|
|
||||||
MXLog.info(mediaInfo)
|
|
||||||
|
|
||||||
switch mediaInfo {
|
|
||||||
case let .image(imageURL, thumbnailURL, imageInfo):
|
|
||||||
let _ = await self?.parameters.roomProxy.sendImage(url: imageURL, thumbnailURL: thumbnailURL, imageInfo: imageInfo)
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case .failure(let error):
|
|
||||||
MXLog.error("Failed processing media to upload with error: \(error)")
|
|
||||||
}
|
|
||||||
|
|
||||||
self?.navigationStackCoordinator.setSheetCoordinator(nil)
|
|
||||||
}
|
|
||||||
case .cancel:
|
|
||||||
self?.navigationStackCoordinator.setSheetCoordinator(nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self?.navigationStackCoordinator.setSheetCoordinator(mediaPickerPreviewScreenCoordinator)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
navigationStackCoordinator.setSheetCoordinator(mediaPickerCoordinator)
|
stackCoordinator.setRootCoordinator(mediaPickerCoordinator)
|
||||||
|
|
||||||
|
navigationStackCoordinator.setSheetCoordinator(userIndicatorController)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func displayMediaUploadPreviewScreenForFile(at url: URL) {
|
||||||
|
let stackCoordinator = NavigationStackCoordinator()
|
||||||
|
let userIndicatorController = UserIndicatorController(rootCoordinator: stackCoordinator)
|
||||||
|
|
||||||
|
let parameters = MediaUploadPreviewScreenCoordinatorParameters(userIndicatorController: userIndicatorController,
|
||||||
|
roomProxy: parameters.roomProxy,
|
||||||
|
mediaUploadingPreprocessor: MediaUploadingPreprocessor(),
|
||||||
|
title: url.lastPathComponent,
|
||||||
|
url: url)
|
||||||
|
|
||||||
|
let mediaUploadPreviewScreenCoordinator = MediaUploadPreviewScreenCoordinator(parameters: parameters) { [weak self] action in
|
||||||
|
switch action {
|
||||||
|
case .dismiss:
|
||||||
|
self?.navigationStackCoordinator.setSheetCoordinator(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stackCoordinator.setRootCoordinator(mediaUploadPreviewScreenCoordinator)
|
||||||
|
|
||||||
|
navigationStackCoordinator.setSheetCoordinator(userIndicatorController)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func displayFilePreview(for file: MediaFileHandleProxy, with title: String?) {
|
private func displayFilePreview(for file: MediaFileHandleProxy, with title: String?) {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ enum RoomScreenViewModelAction {
|
|||||||
case displayCameraPicker
|
case displayCameraPicker
|
||||||
case displayMediaPicker
|
case displayMediaPicker
|
||||||
case displayDocumentPicker
|
case displayDocumentPicker
|
||||||
|
case displayMediaUploadPreviewScreen(url: URL)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum RoomScreenComposerMode: Equatable {
|
enum RoomScreenComposerMode: Equatable {
|
||||||
@@ -62,6 +63,8 @@ enum RoomScreenViewAction {
|
|||||||
case displayCameraPicker
|
case displayCameraPicker
|
||||||
case displayMediaPicker
|
case displayMediaPicker
|
||||||
case displayDocumentPicker
|
case displayDocumentPicker
|
||||||
|
|
||||||
|
case handlePasteOrDrop(provider: NSItemProvider)
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RoomScreenViewState: BindableState {
|
struct RoomScreenViewState: BindableState {
|
||||||
@@ -73,7 +76,6 @@ struct RoomScreenViewState: BindableState {
|
|||||||
var isBackPaginating = false
|
var isBackPaginating = false
|
||||||
var showLoading = false
|
var showLoading = false
|
||||||
var timelineStyle: TimelineStyle
|
var timelineStyle: TimelineStyle
|
||||||
var mediaUploadingFlowEnabled: Bool
|
|
||||||
|
|
||||||
var bindings: RoomScreenViewStateBindings
|
var bindings: RoomScreenViewStateBindings
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
roomTitle: roomName ?? "Unknown room 💥",
|
roomTitle: roomName ?? "Unknown room 💥",
|
||||||
roomAvatarURL: roomAvatarUrl,
|
roomAvatarURL: roomAvatarUrl,
|
||||||
timelineStyle: ServiceLocator.shared.settings.timelineStyle,
|
timelineStyle: ServiceLocator.shared.settings.timelineStyle,
|
||||||
mediaUploadingFlowEnabled: ServiceLocator.shared.settings.mediaUploadingFlowEnabled,
|
|
||||||
bindings: .init(composerText: "", composerFocused: false)),
|
bindings: .init(composerText: "", composerFocused: false)),
|
||||||
imageProvider: mediaProvider)
|
imageProvider: mediaProvider)
|
||||||
|
|
||||||
@@ -75,10 +74,6 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
.weakAssign(to: \.state.timelineStyle, on: self)
|
.weakAssign(to: \.state.timelineStyle, on: self)
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
ServiceLocator.shared.settings.$mediaUploadingFlowEnabled
|
|
||||||
.weakAssign(to: \.state.mediaUploadingFlowEnabled, on: self)
|
|
||||||
.store(in: &cancellables)
|
|
||||||
|
|
||||||
buildTimelineViews()
|
buildTimelineViews()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,6 +117,8 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
callback?(.displayMediaPicker)
|
callback?(.displayMediaPicker)
|
||||||
case .displayDocumentPicker:
|
case .displayDocumentPicker:
|
||||||
callback?(.displayDocumentPicker)
|
callback?(.displayDocumentPicker)
|
||||||
|
case .handlePasteOrDrop(let provider):
|
||||||
|
handlePasteOrDrop(provider)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,6 +331,60 @@ class RoomScreenViewModel: RoomScreenViewModelType, RoomScreenViewModelProtocol
|
|||||||
state.composerMode = .default
|
state.composerMode = .default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pasting and dropping
|
||||||
|
|
||||||
|
private func handlePasteOrDrop(_ provider: NSItemProvider) {
|
||||||
|
guard let type = provider.registeredContentTypes.first,
|
||||||
|
let preferredExtension = type.preferredFilenameExtension else {
|
||||||
|
MXLog.error("Invalid NSItemProvider: \(provider)")
|
||||||
|
displayError(.toast(L10n.screenRoomErrorFailedProcessingMedia))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let providerSuggestedName = provider.suggestedName
|
||||||
|
let providerDescription = provider.description
|
||||||
|
|
||||||
|
_ = provider.loadDataRepresentation(for: type) { data, error in
|
||||||
|
Task { @MainActor in
|
||||||
|
let loadingIndicatorIdentifier = UUID().uuidString
|
||||||
|
ServiceLocator.shared.userIndicatorController.submitIndicator(UserIndicator(id: loadingIndicatorIdentifier, type: .modal, title: L10n.commonLoading, persistent: true))
|
||||||
|
defer {
|
||||||
|
ServiceLocator.shared.userIndicatorController.retractIndicatorWithId(loadingIndicatorIdentifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let error {
|
||||||
|
self.displayError(.toast(L10n.screenRoomErrorFailedProcessingMedia))
|
||||||
|
MXLog.error("Failed processing NSItemProvider: \(providerDescription) with error: \(error)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let data else {
|
||||||
|
self.displayError(.toast(L10n.screenRoomErrorFailedProcessingMedia))
|
||||||
|
MXLog.error("Invalid NSItemProvider data: \(providerDescription)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let url = try await Task.detached {
|
||||||
|
if let filename = providerSuggestedName {
|
||||||
|
let hasExtension = !(filename as NSString).pathExtension.isEmpty
|
||||||
|
let filename = hasExtension ? filename : "\(filename).\(preferredExtension)"
|
||||||
|
return try FileManager.default.writeDataToTemporaryDirectory(data: data, fileName: filename)
|
||||||
|
} else {
|
||||||
|
let filename = "\(UUID().uuidString).\(preferredExtension)"
|
||||||
|
return try FileManager.default.writeDataToTemporaryDirectory(data: data, fileName: filename)
|
||||||
|
}
|
||||||
|
}.value
|
||||||
|
|
||||||
|
self.callback?(.displayMediaUploadPreviewScreen(url: url))
|
||||||
|
} catch {
|
||||||
|
self.displayError(.toast(L10n.screenRoomErrorFailedProcessingMedia))
|
||||||
|
MXLog.error("Failed storing NSItemProvider data \(providerDescription) with error: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Mocks
|
// MARK: - Mocks
|
||||||
|
|||||||
@@ -22,7 +22,8 @@ struct MessageComposer: View {
|
|||||||
let sendingDisabled: Bool
|
let sendingDisabled: Bool
|
||||||
let type: RoomScreenComposerMode
|
let type: RoomScreenComposerMode
|
||||||
|
|
||||||
let sendAction: () -> Void
|
let sendAction: EnterKeyHandler
|
||||||
|
let pasteAction: PasteHandler
|
||||||
let replyCancellationAction: () -> Void
|
let replyCancellationAction: () -> Void
|
||||||
let editCancellationAction: () -> Void
|
let editCancellationAction: () -> Void
|
||||||
|
|
||||||
@@ -39,7 +40,8 @@ struct MessageComposer: View {
|
|||||||
focused: $focused,
|
focused: $focused,
|
||||||
isMultiline: $isMultiline,
|
isMultiline: $isMultiline,
|
||||||
maxHeight: 300,
|
maxHeight: 300,
|
||||||
onEnterKeyHandler: sendAction)
|
enterKeyHandler: sendAction,
|
||||||
|
pasteHandler: pasteAction)
|
||||||
.tint(.element.brand)
|
.tint(.element.brand)
|
||||||
.padding(.vertical, 10)
|
.padding(.vertical, 10)
|
||||||
|
|
||||||
@@ -173,6 +175,7 @@ struct MessageComposer_Previews: PreviewProvider {
|
|||||||
sendingDisabled: true,
|
sendingDisabled: true,
|
||||||
type: .default,
|
type: .default,
|
||||||
sendAction: { },
|
sendAction: { },
|
||||||
|
pasteAction: { _ in },
|
||||||
replyCancellationAction: { },
|
replyCancellationAction: { },
|
||||||
editCancellationAction: { })
|
editCancellationAction: { })
|
||||||
|
|
||||||
@@ -181,6 +184,7 @@ struct MessageComposer_Previews: PreviewProvider {
|
|||||||
sendingDisabled: false,
|
sendingDisabled: false,
|
||||||
type: .default,
|
type: .default,
|
||||||
sendAction: { },
|
sendAction: { },
|
||||||
|
pasteAction: { _ in },
|
||||||
replyCancellationAction: { },
|
replyCancellationAction: { },
|
||||||
editCancellationAction: { })
|
editCancellationAction: { })
|
||||||
|
|
||||||
@@ -189,6 +193,7 @@ struct MessageComposer_Previews: PreviewProvider {
|
|||||||
sendingDisabled: false,
|
sendingDisabled: false,
|
||||||
type: .default,
|
type: .default,
|
||||||
sendAction: { },
|
sendAction: { },
|
||||||
|
pasteAction: { _ in },
|
||||||
replyCancellationAction: { },
|
replyCancellationAction: { },
|
||||||
editCancellationAction: { })
|
editCancellationAction: { })
|
||||||
|
|
||||||
@@ -197,6 +202,7 @@ struct MessageComposer_Previews: PreviewProvider {
|
|||||||
sendingDisabled: false,
|
sendingDisabled: false,
|
||||||
type: .default,
|
type: .default,
|
||||||
sendAction: { },
|
sendAction: { },
|
||||||
|
pasteAction: { _ in },
|
||||||
replyCancellationAction: { },
|
replyCancellationAction: { },
|
||||||
editCancellationAction: { })
|
editCancellationAction: { })
|
||||||
|
|
||||||
@@ -206,6 +212,7 @@ struct MessageComposer_Previews: PreviewProvider {
|
|||||||
type: .reply(id: UUID().uuidString,
|
type: .reply(id: UUID().uuidString,
|
||||||
displayName: "John Doe"),
|
displayName: "John Doe"),
|
||||||
sendAction: { },
|
sendAction: { },
|
||||||
|
pasteAction: { _ in },
|
||||||
replyCancellationAction: { },
|
replyCancellationAction: { },
|
||||||
editCancellationAction: { })
|
editCancellationAction: { })
|
||||||
|
|
||||||
@@ -214,6 +221,7 @@ struct MessageComposer_Previews: PreviewProvider {
|
|||||||
sendingDisabled: false,
|
sendingDisabled: false,
|
||||||
type: .edit(originalItemId: UUID().uuidString),
|
type: .edit(originalItemId: UUID().uuidString),
|
||||||
sendAction: { },
|
sendAction: { },
|
||||||
|
pasteAction: { _ in },
|
||||||
replyCancellationAction: { },
|
replyCancellationAction: { },
|
||||||
editCancellationAction: { })
|
editCancellationAction: { })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,8 @@
|
|||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
typealias OnEnterKeyHandler = () -> Void
|
typealias EnterKeyHandler = () -> Void
|
||||||
|
typealias PasteHandler = (NSItemProvider) -> Void
|
||||||
|
|
||||||
struct MessageComposerTextField: View {
|
struct MessageComposerTextField: View {
|
||||||
let placeholder: String
|
let placeholder: String
|
||||||
@@ -25,7 +26,8 @@ struct MessageComposerTextField: View {
|
|||||||
@Binding var isMultiline: Bool
|
@Binding var isMultiline: Bool
|
||||||
|
|
||||||
let maxHeight: CGFloat
|
let maxHeight: CGFloat
|
||||||
let onEnterKeyHandler: OnEnterKeyHandler
|
let enterKeyHandler: EnterKeyHandler
|
||||||
|
let pasteHandler: PasteHandler
|
||||||
|
|
||||||
private var showingPlaceholder: Bool {
|
private var showingPlaceholder: Bool {
|
||||||
text.isEmpty
|
text.isEmpty
|
||||||
@@ -40,7 +42,8 @@ struct MessageComposerTextField: View {
|
|||||||
focused: $focused,
|
focused: $focused,
|
||||||
isMultiline: $isMultiline,
|
isMultiline: $isMultiline,
|
||||||
maxHeight: maxHeight,
|
maxHeight: maxHeight,
|
||||||
onEnterKeyHandler: onEnterKeyHandler)
|
enterKeyHandler: enterKeyHandler,
|
||||||
|
pasteHandler: pasteHandler)
|
||||||
.accessibilityLabel(placeholder)
|
.accessibilityLabel(placeholder)
|
||||||
.background(placeholderView, alignment: .topLeading)
|
.background(placeholderView, alignment: .topLeading)
|
||||||
}
|
}
|
||||||
@@ -64,15 +67,16 @@ private struct UITextViewWrapper: UIViewRepresentable {
|
|||||||
|
|
||||||
let maxHeight: CGFloat
|
let maxHeight: CGFloat
|
||||||
|
|
||||||
let onEnterKeyHandler: OnEnterKeyHandler
|
let enterKeyHandler: EnterKeyHandler
|
||||||
|
let pasteHandler: PasteHandler
|
||||||
|
|
||||||
private let font = UIFont.preferredFont(forTextStyle: .body)
|
private let font = UIFont.preferredFont(forTextStyle: .body)
|
||||||
|
|
||||||
func makeUIView(context: UIViewRepresentableContext<UITextViewWrapper>) -> UITextView {
|
func makeUIView(context: UIViewRepresentableContext<UITextViewWrapper>) -> UITextView {
|
||||||
let textView = TextViewWithKeyDetection()
|
let textView = ElementTextView()
|
||||||
textView.isMultiline = $isMultiline
|
textView.isMultiline = $isMultiline
|
||||||
textView.delegate = context.coordinator
|
textView.delegate = context.coordinator
|
||||||
textView.keyDelegate = context.coordinator
|
textView.elementDelegate = context.coordinator
|
||||||
textView.textColor = .element.primaryContent
|
textView.textColor = .element.primaryContent
|
||||||
textView.isEditable = true
|
textView.isEditable = true
|
||||||
textView.font = font
|
textView.font = font
|
||||||
@@ -126,25 +130,29 @@ private struct UITextViewWrapper: UIViewRepresentable {
|
|||||||
Coordinator(text: $text,
|
Coordinator(text: $text,
|
||||||
focused: $focused,
|
focused: $focused,
|
||||||
maxHeight: maxHeight,
|
maxHeight: maxHeight,
|
||||||
onEnterKeyHandler: onEnterKeyHandler)
|
enterKeyHandler: enterKeyHandler,
|
||||||
|
pasteHandler: pasteHandler)
|
||||||
}
|
}
|
||||||
|
|
||||||
final class Coordinator: NSObject, UITextViewDelegate, TextViewWithKeyDetectionDelegate {
|
final class Coordinator: NSObject, UITextViewDelegate, ElementTextViewDelegate {
|
||||||
private var text: Binding<String>
|
private var text: Binding<String>
|
||||||
private var focused: Binding<Bool>
|
private var focused: Binding<Bool>
|
||||||
|
|
||||||
private let maxHeight: CGFloat
|
private let maxHeight: CGFloat
|
||||||
|
|
||||||
private let onEnterKeyHandler: OnEnterKeyHandler
|
private let enterKeyHandler: EnterKeyHandler
|
||||||
|
private let pasteHandler: PasteHandler
|
||||||
|
|
||||||
init(text: Binding<String>,
|
init(text: Binding<String>,
|
||||||
focused: Binding<Bool>,
|
focused: Binding<Bool>,
|
||||||
maxHeight: CGFloat,
|
maxHeight: CGFloat,
|
||||||
onEnterKeyHandler: @escaping OnEnterKeyHandler) {
|
enterKeyHandler: @escaping EnterKeyHandler,
|
||||||
|
pasteHandler: @escaping PasteHandler) {
|
||||||
self.text = text
|
self.text = text
|
||||||
self.focused = focused
|
self.focused = focused
|
||||||
self.maxHeight = maxHeight
|
self.maxHeight = maxHeight
|
||||||
self.onEnterKeyHandler = onEnterKeyHandler
|
self.enterKeyHandler = enterKeyHandler
|
||||||
|
self.pasteHandler = pasteHandler
|
||||||
}
|
}
|
||||||
|
|
||||||
func textViewDidChange(_ textView: UITextView) {
|
func textViewDidChange(_ textView: UITextView) {
|
||||||
@@ -159,23 +167,28 @@ private struct UITextViewWrapper: UIViewRepresentable {
|
|||||||
focused.wrappedValue = false
|
focused.wrappedValue = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func enterKeyWasPressed(textView: UITextView) {
|
func textViewDidReceiveEnterKeyPress(_ textView: UITextView) {
|
||||||
onEnterKeyHandler()
|
enterKeyHandler()
|
||||||
}
|
}
|
||||||
|
|
||||||
func shiftEnterKeyPressed(textView: UITextView) {
|
func textViewDidReceiveShiftEnterKeyPress(_ textView: UITextView) {
|
||||||
textView.insertText("\n")
|
textView.insertText("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func textView(_ textView: UITextView, didReceivePasteWith provider: NSItemProvider) {
|
||||||
|
pasteHandler(provider)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private protocol TextViewWithKeyDetectionDelegate: AnyObject {
|
private protocol ElementTextViewDelegate: AnyObject {
|
||||||
func enterKeyWasPressed(textView: UITextView)
|
func textViewDidReceiveShiftEnterKeyPress(_ textView: UITextView)
|
||||||
func shiftEnterKeyPressed(textView: UITextView)
|
func textViewDidReceiveEnterKeyPress(_ textView: UITextView)
|
||||||
|
func textView(_ textView: UITextView, didReceivePasteWith provider: NSItemProvider)
|
||||||
}
|
}
|
||||||
|
|
||||||
private class TextViewWithKeyDetection: UITextView {
|
private class ElementTextView: UITextView {
|
||||||
weak var keyDelegate: TextViewWithKeyDetectionDelegate?
|
weak var elementDelegate: ElementTextViewDelegate?
|
||||||
|
|
||||||
var isMultiline: Binding<Bool>?
|
var isMultiline: Binding<Bool>?
|
||||||
|
|
||||||
@@ -185,11 +198,11 @@ private class TextViewWithKeyDetection: UITextView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc func shiftEnterKeyPressed(sender: UIKeyCommand) {
|
@objc func shiftEnterKeyPressed(sender: UIKeyCommand) {
|
||||||
keyDelegate?.shiftEnterKeyPressed(textView: self)
|
elementDelegate?.textViewDidReceiveShiftEnterKeyPress(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func enterKeyPressed(sender: UIKeyCommand) {
|
@objc func enterKeyPressed(sender: UIKeyCommand) {
|
||||||
keyDelegate?.enterKeyWasPressed(textView: self)
|
elementDelegate?.textViewDidReceiveEnterKeyPress(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func layoutSubviews() {
|
override func layoutSubviews() {
|
||||||
@@ -208,6 +221,31 @@ private class TextViewWithKeyDetection: UITextView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pasting support
|
||||||
|
|
||||||
|
override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
|
||||||
|
if super.canPerformAction(action, withSender: sender) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
guard action == #selector(paste(_:)) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return UIPasteboard.general.itemProviders.first?.isSupportedForPasteOrDrop ?? false
|
||||||
|
}
|
||||||
|
|
||||||
|
override func paste(_ sender: Any?) {
|
||||||
|
super.paste(sender)
|
||||||
|
|
||||||
|
guard let provider = UIPasteboard.general.itemProviders.first,
|
||||||
|
provider.isSupportedForPasteOrDrop else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
elementDelegate?.textView(self, didReceivePasteWith: provider)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct MessageComposerTextField_Previews: PreviewProvider {
|
struct MessageComposerTextField_Previews: PreviewProvider {
|
||||||
@@ -236,7 +274,8 @@ struct MessageComposerTextField_Previews: PreviewProvider {
|
|||||||
focused: $focused,
|
focused: $focused,
|
||||||
isMultiline: $isMultiline,
|
isMultiline: $isMultiline,
|
||||||
maxHeight: 300,
|
maxHeight: 300,
|
||||||
onEnterKeyHandler: { })
|
enterKeyHandler: { },
|
||||||
|
pasteHandler: { _ in })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,98 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2023 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 RoomAttachmentPicker: View {
|
||||||
|
@ObservedObject var context: RoomScreenViewModel.Context
|
||||||
|
|
||||||
|
@State private var showAttachmentPopover = false
|
||||||
|
@State private var sheetContentHeight = CGFloat(0)
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Button {
|
||||||
|
showAttachmentPopover = true
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "plus.circle.fill")
|
||||||
|
.font(.compound.headingLG)
|
||||||
|
.foregroundColor(.element.accent)
|
||||||
|
}
|
||||||
|
.accessibilityIdentifier(A11yIdentifiers.roomScreen.attachmentPicker)
|
||||||
|
.popover(isPresented: $showAttachmentPopover) {
|
||||||
|
VStack(alignment: .leading, spacing: 0.0) {
|
||||||
|
Button {
|
||||||
|
showAttachmentPopover = false
|
||||||
|
context.send(viewAction: .displayMediaPicker)
|
||||||
|
} label: {
|
||||||
|
PickerLabel(title: L10n.screenRoomAttachmentSourceGallery, systemImageName: "photo.fill")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
showAttachmentPopover = false
|
||||||
|
context.send(viewAction: .displayDocumentPicker)
|
||||||
|
} label: {
|
||||||
|
PickerLabel(title: L10n.screenRoomAttachmentSourceFiles, systemImageName: "paperclip")
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
showAttachmentPopover = false
|
||||||
|
context.send(viewAction: .displayCameraPicker)
|
||||||
|
} label: {
|
||||||
|
PickerLabel(title: L10n.screenRoomAttachmentSourceCamera, systemImageName: "camera.fill")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.background {
|
||||||
|
// This is done in the background otherwise GeometryReader tends to expand to
|
||||||
|
// all the space given to it like color or shape.
|
||||||
|
GeometryReader { proxy in
|
||||||
|
Color.clear
|
||||||
|
.onAppear {
|
||||||
|
sheetContentHeight = proxy.size.height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.presentationDetents([.height(sheetContentHeight)])
|
||||||
|
.presentationDragIndicator(.visible)
|
||||||
|
.tint(.element.accent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct PickerLabel: View {
|
||||||
|
let title: String
|
||||||
|
let systemImageName: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Label(title, systemImage: systemImageName)
|
||||||
|
.labelStyle(EqualIconWidthLabelStyle())
|
||||||
|
.multilineTextAlignment(.leading)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct EqualIconWidthLabelStyle: LabelStyle {
|
||||||
|
@ScaledMetric private var menuIconSize = 24.0
|
||||||
|
|
||||||
|
func makeBody(configuration: Configuration) -> some View {
|
||||||
|
HStack {
|
||||||
|
configuration
|
||||||
|
.icon
|
||||||
|
.frame(width: menuIconSize, height: menuIconSize)
|
||||||
|
configuration.title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,16 +19,14 @@ import SwiftUI
|
|||||||
struct RoomScreen: View {
|
struct RoomScreen: View {
|
||||||
@ObservedObject var context: RoomScreenViewModel.Context
|
@ObservedObject var context: RoomScreenViewModel.Context
|
||||||
@State private var showReactionsMenuForItemId = ""
|
@State private var showReactionsMenuForItemId = ""
|
||||||
|
@State private var dragOver = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
timeline
|
timeline
|
||||||
.background(Color.element.background.ignoresSafeArea()) // Kills the toolbar translucency.
|
.background(Color.element.background.ignoresSafeArea()) // Kills the toolbar translucency.
|
||||||
.safeAreaInset(edge: .bottom, spacing: 0) {
|
.safeAreaInset(edge: .bottom, spacing: 0) {
|
||||||
HStack(spacing: 4.0) {
|
HStack(spacing: 4.0) {
|
||||||
if context.viewState.mediaUploadingFlowEnabled {
|
RoomAttachmentPicker(context: context)
|
||||||
sendAttachmentButton
|
|
||||||
}
|
|
||||||
|
|
||||||
messageComposer
|
messageComposer
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
@@ -48,6 +46,15 @@ struct RoomScreen: View {
|
|||||||
guard !Task.isCancelled else { return }
|
guard !Task.isCancelled else { return }
|
||||||
context.send(viewAction: .markRoomAsRead)
|
context.send(viewAction: .markRoomAsRead)
|
||||||
}
|
}
|
||||||
|
.onDrop(of: ["public.item"], isTargeted: $dragOver) { providers -> Bool in
|
||||||
|
guard let provider = providers.first,
|
||||||
|
provider.isSupportedForPasteOrDrop else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
context.send(viewAction: .handlePasteOrDrop(provider: provider))
|
||||||
|
return true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var timeline: some View {
|
private var timeline: some View {
|
||||||
@@ -64,6 +71,8 @@ struct RoomScreen: View {
|
|||||||
sendingDisabled: context.viewState.sendButtonDisabled,
|
sendingDisabled: context.viewState.sendButtonDisabled,
|
||||||
type: context.viewState.composerMode) {
|
type: context.viewState.composerMode) {
|
||||||
sendMessage()
|
sendMessage()
|
||||||
|
} pasteAction: { provider in
|
||||||
|
context.send(viewAction: .handlePasteOrDrop(provider: provider))
|
||||||
} replyCancellationAction: {
|
} replyCancellationAction: {
|
||||||
context.send(viewAction: .cancelReply)
|
context.send(viewAction: .cancelReply)
|
||||||
} editCancellationAction: {
|
} editCancellationAction: {
|
||||||
@@ -113,30 +122,6 @@ struct RoomScreen: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var sendAttachmentButton: some View {
|
|
||||||
Menu {
|
|
||||||
Button {
|
|
||||||
context.send(viewAction: .displayDocumentPicker)
|
|
||||||
} label: {
|
|
||||||
Label(UntranslatedL10n.mediaUploadDocumentPicker, systemImage: "doc")
|
|
||||||
}
|
|
||||||
Button {
|
|
||||||
context.send(viewAction: .displayMediaPicker)
|
|
||||||
} label: {
|
|
||||||
Label(UntranslatedL10n.mediaUploadPhotoAndVideoPicker, systemImage: "photo")
|
|
||||||
}
|
|
||||||
Button {
|
|
||||||
context.send(viewAction: .displayCameraPicker)
|
|
||||||
} label: {
|
|
||||||
Label(UntranslatedL10n.mediaUploadCameraPicker, systemImage: "camera")
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
Image(systemName: "plus.circle")
|
|
||||||
.font(.compound.headingLG)
|
|
||||||
.foregroundColor(.element.brand)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func sendMessage() {
|
private func sendMessage() {
|
||||||
guard !context.viewState.sendButtonDisabled else { return }
|
guard !context.viewState.sendButtonDisabled else { return }
|
||||||
context.send(viewAction: .sendMessage)
|
context.send(viewAction: .sendMessage)
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ struct ImageRoomTimelineView: View {
|
|||||||
imageProvider: context.imageProvider) {
|
imageProvider: context.imageProvider) {
|
||||||
placeholder
|
placeholder
|
||||||
}
|
}
|
||||||
.frame(maxHeight: min(300, timelineItem.height ?? .infinity))
|
.frame(maxHeight: min(300, max(100, timelineItem.height ?? .infinity)))
|
||||||
.aspectRatio(timelineItem.aspectRatio, contentMode: .fit)
|
.aspectRatio(timelineItem.aspectRatio, contentMode: .fit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ struct StickerRoomTimelineView: View {
|
|||||||
imageProvider: context.imageProvider) {
|
imageProvider: context.imageProvider) {
|
||||||
placeholder
|
placeholder
|
||||||
}
|
}
|
||||||
.frame(maxHeight: min(300, timelineItem.height ?? .infinity))
|
.frame(maxHeight: min(300, max(100, timelineItem.height ?? .infinity)))
|
||||||
.aspectRatio(timelineItem.aspectRatio, contentMode: .fit)
|
.aspectRatio(timelineItem.aspectRatio, contentMode: .fit)
|
||||||
}
|
}
|
||||||
.accessibilityLabel(timelineItem.body)
|
.accessibilityLabel(timelineItem.body)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ struct VideoRoomTimelineView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
TimelineStyler(timelineItem: timelineItem) {
|
TimelineStyler(timelineItem: timelineItem) {
|
||||||
thumbnail
|
thumbnail
|
||||||
.frame(maxHeight: min(300, timelineItem.height ?? .infinity))
|
.frame(maxHeight: min(300, max(100, timelineItem.height ?? .infinity)))
|
||||||
.aspectRatio(timelineItem.aspectRatio, contentMode: .fit)
|
.aspectRatio(timelineItem.aspectRatio, contentMode: .fit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ class StartChatScreenViewModel: StartChatScreenViewModelType, StartChatScreenVie
|
|||||||
|
|
||||||
// MARK: Loading indicator
|
// MARK: Loading indicator
|
||||||
|
|
||||||
static let loadingIndicatorIdentifier = "StartChatLoading"
|
private static let loadingIndicatorIdentifier = "StartChatLoading"
|
||||||
|
|
||||||
private func showLoadingIndicator() {
|
private func showLoadingIndicator() {
|
||||||
userIndicatorController?.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier,
|
userIndicatorController?.submitIndicator(UserIndicator(id: Self.loadingIndicatorIdentifier,
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ enum MediaInfo {
|
|||||||
case image(imageURL: URL, thumbnailURL: URL, imageInfo: ImageInfo)
|
case image(imageURL: URL, thumbnailURL: URL, imageInfo: ImageInfo)
|
||||||
case video(videoURL: URL, thumbnailURL: URL, videoInfo: VideoInfo)
|
case video(videoURL: URL, thumbnailURL: URL, videoInfo: VideoInfo)
|
||||||
case audio(audioURL: URL, audioInfo: AudioInfo)
|
case audio(audioURL: URL, audioInfo: AudioInfo)
|
||||||
case file(FileInfo)
|
case file(fileURL: URL, fileInfo: FileInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct ImageProcessingInfo {
|
private struct ImageProcessingInfo {
|
||||||
@@ -205,7 +205,7 @@ struct MediaUploadingPreprocessor {
|
|||||||
let fileSize = try? UInt64(FileManager.default.sizeForItem(at: url))
|
let fileSize = try? UInt64(FileManager.default.sizeForItem(at: url))
|
||||||
|
|
||||||
let fileInfo = FileInfo(mimetype: mimeType, size: fileSize, thumbnailInfo: nil, thumbnailSource: nil)
|
let fileInfo = FileInfo(mimetype: mimeType, size: fileSize, thumbnailInfo: nil, thumbnailSource: nil)
|
||||||
return .success(.file(fileInfo))
|
return .success(.file(fileURL: url, fileInfo: fileInfo))
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Images
|
// MARK: Images
|
||||||
@@ -217,32 +217,30 @@ struct MediaUploadingPreprocessor {
|
|||||||
/// - Returns: the URL for the modified image and its size as an `ImageProcessingResult`
|
/// - Returns: the URL for the modified image and its size as an `ImageProcessingResult`
|
||||||
private func stripLocationFromImage(at url: URL, type: UTType, mimeType: String) async -> Result<ImageProcessingInfo, MediaUploadingPreprocessorError> {
|
private func stripLocationFromImage(at url: URL, type: UTType, mimeType: String) async -> Result<ImageProcessingInfo, MediaUploadingPreprocessorError> {
|
||||||
guard let originalData = NSData(contentsOf: url),
|
guard let originalData = NSData(contentsOf: url),
|
||||||
let originalCGImage = UIImage(data: originalData as Data)?.cgImage,
|
let originalImage = UIImage(data: originalData as Data),
|
||||||
let imageSource = CGImageSourceCreateWithData(originalData, nil) else {
|
let imageSource = CGImageSourceCreateWithData(originalData, nil) else {
|
||||||
return .failure(.failedStrippingLocationData)
|
return .failure(.failedStrippingLocationData)
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let originalMetadata = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) else {
|
guard let originalMetadata = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil),
|
||||||
MXLog.info("No metadata found. Returning original image")
|
(originalMetadata as NSDictionary).value(forKeyPath: "\(kCGImagePropertyGPSDictionary)") != nil else {
|
||||||
return .success(.init(url: url, height: Double(originalCGImage.height), width: Double(originalCGImage.width), mimeType: mimeType, blurhash: nil))
|
MXLog.info("No GPS metadata found. Returning original image")
|
||||||
|
return .success(.init(url: url, height: Double(originalImage.size.height), width: Double(originalImage.size.width), mimeType: mimeType, blurhash: nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let adjustedMetadata = (originalMetadata as NSDictionary).mutableCopy() as? NSMutableDictionary else {
|
let count = CGImageSourceGetCount(imageSource)
|
||||||
return .failure(.failedStrippingLocationData)
|
let metadataKeysToRemove = [kCGImagePropertyGPSDictionary: kCFNull]
|
||||||
}
|
|
||||||
|
|
||||||
adjustedMetadata.setValue(nil, forKeyPath: "\(kCGImagePropertyGPSDictionary)")
|
|
||||||
|
|
||||||
let data = NSMutableData()
|
let data = NSMutableData()
|
||||||
guard let destination = CGImageDestinationCreateWithData(data as CFMutableData, type.identifier as CFString, 1, nil) else {
|
guard let destination = CGImageDestinationCreateWithData(data as CFMutableData, type.identifier as CFString, count, nil) else {
|
||||||
return .failure(.failedStrippingLocationData)
|
return .failure(.failedStrippingLocationData)
|
||||||
}
|
}
|
||||||
CGImageDestinationAddImage(destination, originalCGImage, adjustedMetadata)
|
CGImageDestinationAddImageFromSource(destination, imageSource, 0, metadataKeysToRemove as NSDictionary)
|
||||||
CGImageDestinationFinalize(destination)
|
CGImageDestinationFinalize(destination)
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try data.write(to: url)
|
try data.write(to: url)
|
||||||
return .success(.init(url: url, height: Double(originalCGImage.height), width: Double(originalCGImage.width), mimeType: mimeType, blurhash: nil))
|
return .success(.init(url: url, height: Double(originalImage.size.height), width: Double(originalImage.size.width), mimeType: mimeType, blurhash: nil))
|
||||||
} catch {
|
} catch {
|
||||||
return .failure(.failedStrippingLocationData)
|
return .failure(.failedStrippingLocationData)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -227,20 +227,67 @@ class RoomProxy: RoomProxyProtocol {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func sendImage(url: URL, thumbnailURL: URL, imageInfo: ImageInfo) async -> Result<Void, RoomProxyError> {
|
func sendImage(url: URL, thumbnailURL: URL, imageInfo: ImageInfo) async -> Result<Void, RoomProxyError> {
|
||||||
.failure(.failedSendingMedia)
|
sendMessageBackgroundTask = backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
|
||||||
// sendMessageBackgroundTask = backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
|
defer {
|
||||||
// defer {
|
sendMessageBackgroundTask?.stop()
|
||||||
// sendMessageBackgroundTask?.stop()
|
}
|
||||||
// }
|
|
||||||
//
|
return await Task.dispatch(on: userInitiatedDispatchQueue) {
|
||||||
// return await Task.dispatch(on: userInitiatedDispatchQueue) {
|
do {
|
||||||
// do {
|
try self.room.sendImage(url: url.path(), thumbnailUrl: thumbnailURL.path(), imageInfo: imageInfo)
|
||||||
// try self.room.sendImage(url: url.path(), thumbnailUrl: thumbnailURL.path(), imageInfo: imageInfo)
|
return .success(())
|
||||||
// return .success(())
|
} catch {
|
||||||
// } catch {
|
return .failure(.failedSendingMedia)
|
||||||
// return .failure(.failedEditingMessage)
|
}
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
|
func sendVideo(url: URL, thumbnailURL: URL, videoInfo: VideoInfo) async -> Result<Void, RoomProxyError> {
|
||||||
|
sendMessageBackgroundTask = backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
|
||||||
|
defer {
|
||||||
|
sendMessageBackgroundTask?.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
return await Task.dispatch(on: userInitiatedDispatchQueue) {
|
||||||
|
do {
|
||||||
|
try self.room.sendVideo(url: url.path(), thumbnailUrl: thumbnailURL.path(), videoInfo: videoInfo)
|
||||||
|
return .success(())
|
||||||
|
} catch {
|
||||||
|
return .failure(.failedSendingMedia)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendAudio(url: URL, audioInfo: AudioInfo) async -> Result<Void, RoomProxyError> {
|
||||||
|
sendMessageBackgroundTask = backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
|
||||||
|
defer {
|
||||||
|
sendMessageBackgroundTask?.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
return await Task.dispatch(on: userInitiatedDispatchQueue) {
|
||||||
|
do {
|
||||||
|
try self.room.sendAudio(url: url.path(), audioInfo: audioInfo)
|
||||||
|
return .success(())
|
||||||
|
} catch {
|
||||||
|
return .failure(.failedSendingMedia)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendFile(url: URL, fileInfo: FileInfo) async -> Result<Void, RoomProxyError> {
|
||||||
|
sendMessageBackgroundTask = backgroundTaskService.startBackgroundTask(withName: backgroundTaskName, isReusable: true)
|
||||||
|
defer {
|
||||||
|
sendMessageBackgroundTask?.stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
return await Task.dispatch(on: userInitiatedDispatchQueue) {
|
||||||
|
do {
|
||||||
|
try self.room.sendFile(url: url.path(), fileInfo: fileInfo)
|
||||||
|
return .success(())
|
||||||
|
} catch {
|
||||||
|
return .failure(.failedSendingMedia)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func editMessage(_ newMessage: String, original eventID: String) async -> Result<Void, RoomProxyError> {
|
func editMessage(_ newMessage: String, original eventID: String) async -> Result<Void, RoomProxyError> {
|
||||||
|
|||||||
@@ -77,6 +77,12 @@ protocol RoomProxyProtocol {
|
|||||||
|
|
||||||
func sendImage(url: URL, thumbnailURL: URL, imageInfo: ImageInfo) async -> Result<Void, RoomProxyError>
|
func sendImage(url: URL, thumbnailURL: URL, imageInfo: ImageInfo) async -> Result<Void, RoomProxyError>
|
||||||
|
|
||||||
|
func sendVideo(url: URL, thumbnailURL: URL, videoInfo: VideoInfo) async -> Result<Void, RoomProxyError>
|
||||||
|
|
||||||
|
func sendAudio(url: URL, audioInfo: AudioInfo) async -> Result<Void, RoomProxyError>
|
||||||
|
|
||||||
|
func sendFile(url: URL, fileInfo: FileInfo) async -> Result<Void, RoomProxyError>
|
||||||
|
|
||||||
func editMessage(_ newMessage: String, original eventID: String) async -> Result<Void, RoomProxyError>
|
func editMessage(_ newMessage: String, original eventID: String) async -> Result<Void, RoomProxyError>
|
||||||
|
|
||||||
func redact(_ eventID: String) async -> Result<Void, RoomProxyError>
|
func redact(_ eventID: String) async -> Result<Void, RoomProxyError>
|
||||||
|
|||||||
+1
-1
@@ -17,4 +17,4 @@
|
|||||||
import ElementX
|
import ElementX
|
||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
class MediaPickerPreviewScreenUITests: XCTestCase { }
|
class MediaUploadPreviewScreenUITests: XCTestCase { }
|
||||||
@@ -33,5 +33,9 @@ class UserSessionScreenTests: XCTestCase {
|
|||||||
try await Task.sleep(for: .seconds(1))
|
try await Task.sleep(for: .seconds(1))
|
||||||
|
|
||||||
app.assertScreenshot(.userSessionScreen, step: 2)
|
app.assertScreenshot(.userSessionScreen, step: 2)
|
||||||
|
|
||||||
|
app.buttons[A11yIdentifiers.roomScreen.attachmentPicker].tap()
|
||||||
|
|
||||||
|
app.assertScreenshot(.userSessionScreen, step: 3)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:5cb9894274818fc787e8368059ee250b1116b86c007d9e6120c0cdfb662a9b1f
|
oid sha256:6fb7862fb96c989a30077ea6a91c1b550b6bff2dec6095ecba54813a4e87a91e
|
||||||
size 225450
|
size 226885
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:9e530096c62b5eed931df88a4a0a229130c2b3007cee614706f007a81e5fbf76
|
oid sha256:9f18fbb5e3246cfcc7d73448dd28c88ffd2cec2de5143e151d36228f54286bc8
|
||||||
size 277101
|
size 279203
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:3ec5385d592d1067b7f8afda9dfa56ec21a94d54eb7c3351078ce9ff1b64624c
|
oid sha256:0542a656e0a35a24e668367fae91963f5d9fa742d530051748ed6221b8a2bf5a
|
||||||
size 286210
|
size 287156
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:c79ad9e4df49dfbd266248571d082e98852d1a2a02d743a413281a3b08dd3068
|
oid sha256:eb1c54d3f5315093be786051ad4e53579c2c51f4b79b1bb1f21baea95a20f1f5
|
||||||
size 285382
|
size 287904
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:ac2bfda0c854002deb4b220fd95588a711d5bc80832cf51dcd68d1b834215883
|
oid sha256:376ec8525fcc28da894f231b6538923feeba0a296b531d0b8d214bba769a94ae
|
||||||
size 298817
|
size 301762
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:30ae9c3b1d985b20bcb233a6509fcacd20b889935a210afab837ce2849954d81
|
oid sha256:2781f8b12a7c359ed0aba73b2601d713a152a4c6fdd1a76115d6c214c1bf4a8c
|
||||||
size 263931
|
size 265581
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:6d1aab4750d55c1c59b9da255f5ce28f7d49d85bac9f5776faaef25991b2a6d2
|
oid sha256:c0a6edbb32843b33c62cb0ab10a0c461c0704bb75c61e20f37a7a57c517a3a52
|
||||||
size 225335
|
size 226770
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:73b62c2a828f6ba3a2c30f7d1301125b8a585e4b3541cdd126a30503085d1c89
|
oid sha256:f62df61b8daf07b747463042153b0726d0aff05b439978524316ebac52229ecc
|
||||||
size 94176
|
size 95571
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:c544755cdbe2f6f3bfb6529660f6ccec8664dac5e43e6bfbea873750500952b5
|
oid sha256:a03a58ed11fef52767c27fbf6475f8ca91de31ecc0543e8861a27f30b7b62793
|
||||||
size 115325
|
size 116521
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:2e14f8fae9d1b7dcf6add882eb5f064ca6de62bdc5b919c3f122c9d3cb0d7028
|
oid sha256:a630be82b6722a76407e608018e75b1798a218bcf7aaf25e5908c6f96862e274
|
||||||
size 282835
|
size 284402
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:a223ca272eb48269cc08ddba430fd86b3fe42bd60ce8c1bd3c26578e15ab9b4d
|
oid sha256:efb8498c7f77bb8df8992eedd914b371ac5c7ed8351fc5c392e2b58b5f614663
|
||||||
size 154469
|
size 154248
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:610b10157a0ee583bc273031002d39894e5355f30568a287d502d93e7a327c2f
|
oid sha256:bf27bfef228ca1b4b0c043cd8d5034b61b3f4780475f5bc2ebe43273d288a54b
|
||||||
size 313574
|
size 315147
|
||||||
|
|||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:0970c28baf874cd88eddf8e3da907dde4e9702ea34c2cdef1138c911b7f570a0
|
||||||
|
size 382923
|
||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:ddfcc3aa97708a061e822dd6c3230c57582ccbdf3d5d3a53a30008743ac5a5d4
|
oid sha256:a66665604f91d490c85acaba6941de9dbc4ca772bd079eabbd78efc821841886
|
||||||
size 346656
|
size 349022
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:307248a447fe5928ca89b0d2fd8157e246d91c62544e045a6ff21d159dcb544e
|
oid sha256:0b430246471c4855584c8d12ef502e3e5c589b4c8c379a1e7451ea5fc0667d16
|
||||||
size 286725
|
size 289146
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:1d8cc9c1c14b8e6d5e1b2565bb72b0df204b66247f6e6c691084bc12dad8005a
|
oid sha256:de61686d433b8b952c1e59d46d426886d8d309fd51e05e675bbbef19302e9d38
|
||||||
size 282994
|
size 283677
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:7b91eeb19fe0f391850071b61630c6cd4fa9e933ac58b64eda464ae9448d9ad2
|
oid sha256:db9a008bb006b51adcc1bb2be1d0fe33c95b21f8047eb2f3bbdccbf81de2b7c4
|
||||||
size 310727
|
size 313462
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:e2530011c5753433e2f339d37df7b8ac73b00d17cb0123762bf0c2286dce423d
|
oid sha256:3ef5c1c5f10457803390c17cc3fd6dde17d8703e8b12bdbba385d506298bf604
|
||||||
size 293299
|
size 296864
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:ea7e78a3ebda280ac973dc508b3b0252fe2f2a34278a28f3445165eaa3d503c6
|
oid sha256:4f20d74ceaa84e10c81ec2be3a2a8cd272f7beeeb9dc1c2938ff24b838d4c4dd
|
||||||
size 239496
|
size 241625
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:2eb94dab1ca62737f3f061e0d1db36402792e9233357175cf7dab3f1829ab63e
|
oid sha256:cda777f0e512812d9f7fcd35558c8d80e04f920076378a6877e026aabfec8a18
|
||||||
size 346412
|
size 348778
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:685d500c8651d82669558f86392a772ea26822fe3b9a1412cc27abffc305d29c
|
oid sha256:5a56ebd80da6117480d23ca1ed8014ce47c45535955918680952ebf73f9116a4
|
||||||
size 146765
|
size 147486
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:c827050844076da8a5258bb25a8b15df22331bb72c4138af1bd7b437d175f5fa
|
oid sha256:f2a7499922b564356c7b979a70822ef661a2e0ab683a287ea36d45b10b8b9def
|
||||||
size 180091
|
size 181749
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:203b0151d0d7c6992814526a0e5f8515b62bcb0c16f1747cdaf5ef9bbb95f2af
|
oid sha256:c89e1ad93fc384c9f411dc7c30c5b3f62f07728e73d833755fa52d5079dcfa98
|
||||||
size 298720
|
size 301035
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:f004a204fa567dcf4fb84656c76f6bd6df014c5ae835ab12c5255b7038896c56
|
oid sha256:7943a36b71c55df9a3d079afc47c85ce28e7df85f51cef7e4a162fb27fd31d3b
|
||||||
size 124514
|
size 125709
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:902cb334bc4e2575aaeb6aea3362d9bf9c0b0fc445b0fa3c34ce0ea4cba5e48f
|
oid sha256:79a626a822188e1feecac4489e341155ecbeac9106537186f48a765c5ba093b5
|
||||||
size 289982
|
size 290518
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:b34012b9a354f4ce79fe0cb2c37c31bb5a6e80cef2e6277de57cd2bab6a1789b
|
||||||
|
size 276121
|
||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:a34d12d562035bcfdfb7f3db59f3c5ad39a177a975e1cd308432ed26f05c8d82
|
oid sha256:2733eeae5ecc6a5bf22e7a7e5426ddec69a78ed0952c4b7220e5910ccaae7ae9
|
||||||
size 226595
|
size 228223
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d075bddff47c4046cb35503d423b86aab817c2019a0c42eaa6c6f38d7dca4181
|
oid sha256:19998062e4ccdab92a50fe021810ebe89d582a3b59b3d17c11199d60c0898730
|
||||||
size 278074
|
size 279683
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:0246bc88c988c41e203c47ab9cabd13ca3277a2b940eca722e2024e7348119f7
|
oid sha256:92fafbe4bd93ab79c4e3dcdac286040290136ba38dfdf9810d2f5bda06831f30
|
||||||
size 287184
|
size 289204
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:2917480684b8e48c6ae753a438c05cce44ae362947dee8658325749d506ca27c
|
oid sha256:f3bdcf39e0536fd9627e375f4b3b5b2cddf9bc64e54ea991f3a7e702d0f40f44
|
||||||
size 286818
|
size 288374
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:7973327e95a3ebcdd6bfe90ca56ed2d966fe9f53dda5098ddaad43e8b1269fa5
|
oid sha256:071fc0a6b8f82c7b755beef281cb8d9f3b193002f33df0fddaab82bc3e27c84c
|
||||||
size 300470
|
size 301337
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:20ea438ea8e6e2a3c0e0ce311bf5db75b629b981ea4f2d12a289a2df8f6c4413
|
oid sha256:61c050c837145b2b526e914fd60f2a8f4a99f2eef59b88135e8753e76973b17c
|
||||||
size 264433
|
size 265983
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:fc4586d13995d4626856f66cc56437811de7e183e1eacf762877007c2ad8dd4a
|
oid sha256:385cc5ee090f85667a70689fc46a212d0dce020d2b381b8fc29c1d678a27ba22
|
||||||
size 226475
|
size 228111
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:28d48a3e95753033af12d4ced277763352c7469bebd34e83b5340830d38d88e5
|
oid sha256:6123d80a2e17087302f0703483235e55ca10a1b1d145ef24af17e1d70ddc16f2
|
||||||
size 94563
|
size 96064
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:d6bc5567c37cf3729e5aff93297fbe47cbf51302d57e3bcafa7550ddd26f4fdb
|
oid sha256:b99b3cc08cfc075ce0e1227c120664fe0180e44c37254907630316b64837f5de
|
||||||
size 115469
|
size 116997
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:7b6e9e48310fe7b00d107a56e3e7d0dc8cc45fade058c7f5a8d2718050b2e816
|
oid sha256:d4d7392606a1eeb82c847ad170e6b0d7e0059eb65b1b7d02bb61a8a9188d17ad
|
||||||
size 283307
|
size 284875
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:57fe94d6a90b4bee1e4e63baee4b4fe9a5470fb9c33ae98db3aa488cd3eedd65
|
oid sha256:bd3e0b1dee7ecfaf609cbdb0cf4f23f5223f1e8be35ba7f3546ddc4fca8cb88a
|
||||||
size 156069
|
size 156167
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:7b684cb5897525d433e9693a2f243a3dcb42403c5d02a1ed8ffcc4d3b03e49c2
|
oid sha256:1b1161bb1ad7fc1db9c996247a82bf62a18519dfa061669deef9c0557d92b845
|
||||||
size 315804
|
size 317302
|
||||||
|
|||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:5be7ed8b335671e421a89cc6bfbd2d2e297d090dda6cd6cefa4a01727e29600a
|
||||||
|
size 372076
|
||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:9b97fe382eb5918747ae2475718adcfc4a4b000a2a5053201d4c2b8c1f675421
|
oid sha256:15b2a6aaa9b256eaaa580843fd089825067ff1501a78376a4504d7bfcc690493
|
||||||
size 347284
|
size 349600
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:64f0b68593f9e4c3abc475aa0b4469edbf16d735bb56f2e5288179cdfe273679
|
oid sha256:141a671733a34c53f69f3dc54138af9728de5bf6dce1745fa3f2f1a39a5b022f
|
||||||
size 287438
|
size 289721
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:3cee2ea0dda464cbaef1ae9b0d9a03dbd5af98274a490ceef3343dc0c92d2f41
|
oid sha256:cab131f2cb02a1406928be3b7a2e926d4f861a2ca2df4bd0df33330246a3d505
|
||||||
size 283893
|
size 286283
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:1d03bc917291037f8daea11687d48a0043bd9cae93d8dd0527936657e77a4b40
|
oid sha256:3bffe545a2633bd37a91289115718ccc3ddf97ea96a0d5a1c96d9a3b10c08455
|
||||||
size 311816
|
size 313986
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:4cc60e1c748a48f579f657bdbf63b9141171e29c3a964ced38f848dce21192ee
|
oid sha256:10a42f8557a3022090df12e1553f2f0b785bb9256dd27a8b218c1ec101b634b9
|
||||||
size 295106
|
size 296693
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:6ee52ccb6cb98d52ca7e24c6c81b66a6b35374fb308cc65d4c64fdce6a051ed2
|
oid sha256:f3b361a6c045cf067e788ff7726e5ad821c4d57796a40fcbc17296002955fab6
|
||||||
size 239928
|
size 242272
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:c9164bbfd4316c2f83ead183e8ca54c3443269ede10614e8709c8b3ba21bf449
|
oid sha256:5a936985a9dfc97a33afc709404cad0661a9801f23dd64f56a334c81da981c23
|
||||||
size 347040
|
size 349356
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:1ed637439d696f6591a1799389c0aedce1a827e636de5ddda26674f20b1edd09
|
oid sha256:49070bf6438d39cf70eed78f4fcfee31824ec97f2b80dffea5b4557827336667
|
||||||
size 147189
|
size 148046
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:98dba29c2b606c86771f7617f913d943de9aa8dc267fff374dd3f4c578b27d2c
|
oid sha256:724f9e598a2e0ae454536de50cca057eb6f3d880b845fdac876c7a34d500c5a5
|
||||||
size 180186
|
size 182302
|
||||||
|
|||||||
+2
-2
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:8b12aff9447978bf7908acf27770bfcd5786b36168a354377dabefe93c77e672
|
oid sha256:d4e2d7ccab6079a25d9a7692f2aad2e6bc82f4643f4a8703b92cc561f5723528
|
||||||
size 299340
|
size 301620
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:2ed2cf85759dba6561502d72b6c46ed0c4e7d40a65de066c5fc1346f567707ab
|
oid sha256:4752cf1669531b5faa275e79dcb44cbcdb9c320867ed944b1cd966f379cef2db
|
||||||
size 128637
|
size 128774
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
version https://git-lfs.github.com/spec/v1
|
version https://git-lfs.github.com/spec/v1
|
||||||
oid sha256:657d8fac3e3ad59e7bd304f92dd944591e3ded2d30ef44901c20e4755f280b42
|
oid sha256:e9fc9261b97d9e240b92b5e9dd741c934b783059f50b3aa8fb10c292272b5aa0
|
||||||
size 289714
|
size 290789
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:71b7f71e0b5118067b932b3151bc1bddb4831657e958428536b3dbdd2b1dc211
|
||||||
|
size 283228
|
||||||
+1
-1
@@ -19,4 +19,4 @@ import XCTest
|
|||||||
@testable import ElementX
|
@testable import ElementX
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
class MediaPickerPreviewScreenViewModelTests: XCTestCase { }
|
class MediaUploadPreviewScreenViewModelTests: XCTestCase { }
|
||||||
Reference in New Issue
Block a user