From 978f8d2d5ef909d18bb8bf53fdebf7e74be9d366 Mon Sep 17 00:00:00 2001 From: kkostiuk <kateryna.kostiuk@savoirfairelinux.com> Date: Wed, 21 Apr 2021 11:38:18 -0400 Subject: [PATCH] conversation: actions for interactions 1. enable image zoom 2. update actions for interactions: - resend: for failed interactions - preview: for completed file transfers - share: for completed file transfers - forward: for completed transfer and for all messages - save to gallery: for completed image transfers Change-Id: Ie31f745d52780229bf3260a358af8f6e0279f2a0 --- .../ContactPickerViewController.storyboard | 24 ++-- .../ContactPickerViewController.swift | 3 + Ring/Ring/Constants/Generated/Images.swift | 3 + Ring/Ring/Constants/Generated/Strings.swift | 10 ++ .../Conversation/Cells/MessageCell.swift | 90 ++++++++++-- .../ConversationViewController.swift | 136 ++++++++++++++---- .../Conversation/ConversationViewModel.swift | 34 ++++- .../Conversation/MessageViewModel.swift | 1 + .../Preview/PreviewViewController.storyboard | 66 ++++++++- .../Preview/PreviewViewController.swift | 75 ++++++++++ .../Protocols/ConversationNavigation.swift | 9 +- .../Resources/Images.xcassets/Contents.json | 6 +- .../Contents.json | 16 ++- .../delete-1.png | Bin 391 -> 0 bytes .../delete-2.png | Bin 272 -> 0 bytes .../delete.png | Bin 306 -> 0 bytes .../trash-can-outline-2.png | Bin 0 -> 562 bytes .../trash-can-outline-3.png | Bin 0 -> 769 bytes .../trash-can-outline.png | Bin 0 -> 481 bytes .../ic_forward.imageset/Contents.json | 23 +++ .../ic_forward.imageset/share-outline-2.png | Bin 0 -> 834 bytes .../ic_forward.imageset/share-outline-4.png | Bin 0 -> 1188 bytes .../ic_forward.imageset/share-outline.png | Bin 0 -> 547 bytes .../ic_save.imageset/Contents.json | 26 ++++ .../ic_save.imageset/tray-arrow-down-2.png | Bin 0 -> 760 bytes .../ic_save.imageset/tray-arrow-down-4.png | Bin 0 -> 1092 bytes .../ic_save.imageset/tray-arrow-down.png | Bin 0 -> 546 bytes .../ic_share.imageset/Contents.json | 23 +++ .../ic_share.imageset/export-variant-3.png | Bin 0 -> 386 bytes .../ic_share.imageset/export-variant-4.png | Bin 0 -> 557 bytes .../ic_share.imageset/export-variant.png | Bin 0 -> 713 bytes .../Resources/en.lproj/Localizable.strings | 5 + 32 files changed, 476 insertions(+), 74 deletions(-) delete mode 100644 Ring/Ring/Resources/Images.xcassets/ic_conversation_remove.imageset/delete-1.png delete mode 100644 Ring/Ring/Resources/Images.xcassets/ic_conversation_remove.imageset/delete-2.png delete mode 100644 Ring/Ring/Resources/Images.xcassets/ic_conversation_remove.imageset/delete.png create mode 100644 Ring/Ring/Resources/Images.xcassets/ic_conversation_remove.imageset/trash-can-outline-2.png create mode 100644 Ring/Ring/Resources/Images.xcassets/ic_conversation_remove.imageset/trash-can-outline-3.png create mode 100644 Ring/Ring/Resources/Images.xcassets/ic_conversation_remove.imageset/trash-can-outline.png create mode 100644 Ring/Ring/Resources/Images.xcassets/ic_forward.imageset/Contents.json create mode 100644 Ring/Ring/Resources/Images.xcassets/ic_forward.imageset/share-outline-2.png create mode 100644 Ring/Ring/Resources/Images.xcassets/ic_forward.imageset/share-outline-4.png create mode 100644 Ring/Ring/Resources/Images.xcassets/ic_forward.imageset/share-outline.png create mode 100644 Ring/Ring/Resources/Images.xcassets/ic_save.imageset/Contents.json create mode 100644 Ring/Ring/Resources/Images.xcassets/ic_save.imageset/tray-arrow-down-2.png create mode 100644 Ring/Ring/Resources/Images.xcassets/ic_save.imageset/tray-arrow-down-4.png create mode 100644 Ring/Ring/Resources/Images.xcassets/ic_save.imageset/tray-arrow-down.png create mode 100644 Ring/Ring/Resources/Images.xcassets/ic_share.imageset/Contents.json create mode 100644 Ring/Ring/Resources/Images.xcassets/ic_share.imageset/export-variant-3.png create mode 100644 Ring/Ring/Resources/Images.xcassets/ic_share.imageset/export-variant-4.png create mode 100644 Ring/Ring/Resources/Images.xcassets/ic_share.imageset/export-variant.png diff --git a/Ring/Ring/Calls/Conference/ContactPickerViewController.storyboard b/Ring/Ring/Calls/Conference/ContactPickerViewController.storyboard index 2e3931788..b709a859d 100644 --- a/Ring/Ring/Calls/Conference/ContactPickerViewController.storyboard +++ b/Ring/Ring/Calls/Conference/ContactPickerViewController.storyboard @@ -1,9 +1,9 @@ <?xml version="1.0" encoding="UTF-8"?> -<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16097.3" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="ZwP-Qn-oLY"> +<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="ZwP-Qn-oLY"> <device id="retina6_1" orientation="portrait" appearance="light"/> <dependencies> <deployment identifier="iOS"/> - <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/> + <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17703"/> <capability name="Safe area layout guides" minToolsVersion="9.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> </dependencies> @@ -11,7 +11,7 @@ <!--Contact Picker View Controller--> <scene sceneID="QZd-Vi-EyD"> <objects> - <viewController modalPresentationStyle="overCurrentContext" id="ZwP-Qn-oLY" customClass="ContactPickerViewController" customModule="Ring" customModuleProvider="target" sceneMemberID="viewController"> + <viewController extendedLayoutIncludesOpaqueBars="YES" modalPresentationStyle="overFullScreen" id="ZwP-Qn-oLY" customClass="ContactPickerViewController" customModule="Ring" customModuleProvider="target" sceneMemberID="viewController"> <view key="view" contentMode="scaleToFill" id="TtT-WG-OAE"> <rect key="frame" x="0.0" y="0.0" width="414" height="896"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> @@ -40,10 +40,16 @@ <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="Bdz-kj-0K4"> <rect key="frame" x="0.0" y="44" width="414" height="818"/> <subviews> + <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="LZ0-vA-uc5"> + <rect key="frame" x="182" y="0.0" width="50" height="50"/> + <constraints> + <constraint firstAttribute="height" constant="50" id="C1b-Mo-dnW"/> + </constraints> + </view> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="cfK-DS-kt1"> - <rect key="frame" x="0.0" y="0.0" width="414" height="90"/> + <rect key="frame" x="0.0" y="50" width="414" height="90"/> <subviews> - <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="6AM-a0-weG"> + <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="6AM-a0-weG"> <rect key="frame" x="364" y="45" width="30" height="35"/> <fontDescription key="fontDescription" type="system" pointSize="19"/> </button> @@ -55,7 +61,7 @@ </constraints> </view> <searchBar contentMode="redraw" searchBarStyle="prominent" showsSearchResultsButton="YES" translatesAutoresizingMaskIntoConstraints="NO" id="ht5-JP-L4t"> - <rect key="frame" x="0.0" y="90" width="414" height="44"/> + <rect key="frame" x="0.0" y="140" width="414" height="44"/> <constraints> <constraint firstAttribute="height" constant="44" id="Ott-Go-5mf"/> </constraints> @@ -63,7 +69,7 @@ <textInputTraits key="textInputTraits"/> </searchBar> <tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" allowsMultipleSelection="YES" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="i1P-Li-H82"> - <rect key="frame" x="0.0" y="134" width="414" height="684"/> + <rect key="frame" x="0.0" y="184" width="414" height="634"/> <color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <connections> <outlet property="delegate" destination="ZwP-Qn-oLY" id="EoQ-WF-bN4"/> @@ -71,7 +77,6 @@ </tableView> </subviews> <constraints> - <constraint firstItem="cfK-DS-kt1" firstAttribute="top" secondItem="Bdz-kj-0K4" secondAttribute="top" id="4Ai-6C-pS6"/> <constraint firstAttribute="trailing" secondItem="i1P-Li-H82" secondAttribute="trailing" id="4SM-vM-mgc"/> <constraint firstAttribute="bottom" secondItem="i1P-Li-H82" secondAttribute="bottom" id="MtY-d8-Qac"/> <constraint firstItem="cfK-DS-kt1" firstAttribute="leading" secondItem="Bdz-kj-0K4" secondAttribute="leading" id="RVA-PE-P1b"/> @@ -82,6 +87,7 @@ </constraints> </stackView> </subviews> + <viewLayoutGuide key="safeArea" id="HLr-8o-AJK"/> <constraints> <constraint firstItem="VI3-Wm-odB" firstAttribute="top" secondItem="Bdz-kj-0K4" secondAttribute="top" id="0qq-2Q-k11"/> <constraint firstItem="VI3-Wm-odB" firstAttribute="leading" secondItem="Bdz-kj-0K4" secondAttribute="leading" id="5pC-zY-2Vk"/> @@ -92,12 +98,12 @@ <constraint firstItem="HLr-8o-AJK" firstAttribute="bottom" secondItem="VI3-Wm-odB" secondAttribute="bottom" id="l3e-iB-s2n"/> <constraint firstItem="VI3-Wm-odB" firstAttribute="trailing" secondItem="Bdz-kj-0K4" secondAttribute="trailing" id="sVI-Ny-tp3"/> </constraints> - <viewLayoutGuide key="safeArea" id="HLr-8o-AJK"/> </view> <connections> <outlet property="doneButton" destination="6AM-a0-weG" id="2yX-Fh-qwe"/> <outlet property="searchBar" destination="ht5-JP-L4t" id="adL-r1-B3M"/> <outlet property="tableView" destination="i1P-Li-H82" id="Hbd-c3-3WV"/> + <outlet property="topSpace" destination="C1b-Mo-dnW" id="0UO-Q0-o1H"/> <outlet property="topViewContainer" destination="cfK-DS-kt1" id="s5H-k8-YXw"/> </connections> </viewController> diff --git a/Ring/Ring/Calls/Conference/ContactPickerViewController.swift b/Ring/Ring/Calls/Conference/ContactPickerViewController.swift index 8c85ad965..1ceca8c43 100644 --- a/Ring/Ring/Calls/Conference/ContactPickerViewController.swift +++ b/Ring/Ring/Calls/Conference/ContactPickerViewController.swift @@ -37,6 +37,7 @@ class ContactPickerViewController: UIViewController, StoryboardBased, ViewModelB @IBOutlet weak var tableView: UITableView! @IBOutlet weak var doneButton: UIButton! @IBOutlet weak var topViewContainer: UIView! + @IBOutlet weak var topSpace: NSLayoutConstraint! var viewModel: ContactPickerViewModel! private let disposeBag = DisposeBag() @@ -224,6 +225,7 @@ class ContactPickerViewController: UIViewController, StoryboardBased, ViewModelB let dismissGR = UISwipeGestureRecognizer(target: self, action: #selector(remove(gesture:))) dismissGR.direction = UISwipeGestureRecognizer.Direction.down dismissGR.delegate = self + topSpace.constant = 0 self.searchBar.addGestureRecognizer(dismissGR) self.rowSelectionHandler = { [weak self] row in guard let self = self else { return } @@ -236,6 +238,7 @@ class ContactPickerViewController: UIViewController, StoryboardBased, ViewModelB self.searchBar.backgroundColor = UIColor.clear self.doneButton.setTitle(L10n.Actions.cancelAction, for: .normal) self.doneButton.setTitleColor(UIColor.jamiTextBlue, for: .normal) + topSpace.constant = 50 self.doneButton.rx.tap .subscribe(onNext: { [weak self] in let paths = self?.tableView.indexPathsForSelectedRows diff --git a/Ring/Ring/Constants/Generated/Images.swift b/Ring/Ring/Constants/Generated/Images.swift index f346b3137..adc0e7473 100644 --- a/Ring/Ring/Constants/Generated/Images.swift +++ b/Ring/Ring/Constants/Generated/Images.swift @@ -50,7 +50,10 @@ internal enum Asset { internal static let icBack = ImageAsset(name: "ic_back") internal static let icContactPicture = ImageAsset(name: "ic_contact_picture") internal static let icConversationRemove = ImageAsset(name: "ic_conversation_remove") + internal static let icForward = ImageAsset(name: "ic_forward") internal static let icHideInput = ImageAsset(name: "ic_hide_input") + internal static let icSave = ImageAsset(name: "ic_save") + internal static let icShare = ImageAsset(name: "ic_share") internal static let icShowInput = ImageAsset(name: "ic_show_input") internal static let infoArrow = ImageAsset(name: "info_arrow") internal static let jamiIcon = ImageAsset(name: "jamiIcon") diff --git a/Ring/Ring/Constants/Generated/Strings.swift b/Ring/Ring/Constants/Generated/Strings.swift index 532c2bb4e..17e9c272b 100644 --- a/Ring/Ring/Constants/Generated/Strings.swift +++ b/Ring/Ring/Constants/Generated/Strings.swift @@ -321,6 +321,8 @@ internal enum L10n { } internal enum Conversation { + /// Failed to save image to galery + internal static let errorSavingImage = L10n.tr("Localizable", "conversation.errorSavingImage") /// You are currently receiving a live location from internal static let explanationReceivingLocationFrom = L10n.tr("Localizable", "conversation.explanationReceivingLocationFrom") /// You are currently sharing your location with @@ -451,12 +453,20 @@ internal enum L10n { internal static let close = L10n.tr("Localizable", "global.close") /// Invitations internal static let contactRequestsTabBarTitle = L10n.tr("Localizable", "global.contactRequestsTabBarTitle") + /// Forward + internal static let forward = L10n.tr("Localizable", "global.forward") /// Conversations internal static let homeTabBarTitle = L10n.tr("Localizable", "global.homeTabBarTitle") /// Account internal static let meTabBarTitle = L10n.tr("Localizable", "global.meTabBarTitle") /// Ok internal static let ok = L10n.tr("Localizable", "global.ok") + /// Preview + internal static let preview = L10n.tr("Localizable", "global.preview") + /// Resend + internal static let resend = L10n.tr("Localizable", "global.resend") + /// Save + internal static let save = L10n.tr("Localizable", "global.save") /// Share internal static let share = L10n.tr("Localizable", "global.share") /// Together diff --git a/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCell.swift b/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCell.swift index c54f98c38..36579cbc4 100644 --- a/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCell.swift +++ b/Ring/Ring/Features/Conversations/Conversation/Cells/MessageCell.swift @@ -30,8 +30,7 @@ import SwiftyBeaver // swiftlint:disable type_body_length // swiftlint:disable file_length -class MessageCell: UITableViewCell, NibReusable, PlayerDelegate { - +class MessageCell: UITableViewCell, NibReusable, PlayerDelegate, PreviewViewControllerDelegate { // MARK: Properties let log = SwiftyBeaver.self @@ -76,10 +75,17 @@ class MessageCell: UITableViewCell, NibReusable, PlayerDelegate { private(set) var messageId: Int64? private var isCopyable: Bool = false private var couldBeShared: Bool = false + private var couldBeResend: Bool = false + private var couldBeForward: Bool = false + private var couldBeSaved: Bool = false private let _deleteMessage = BehaviorRelay<Bool>(value: false) var deleteMessage: Observable<Bool> { _deleteMessage.asObservable() } var shareMessage = PublishSubject<Bool>() + var forwardMessage = PublishSubject<Bool>() + var saveMessage = PublishSubject<Bool>() + var resendMessage = PublishSubject<Bool>() + var previewMessage = PublishSubject<Bool>() private let showTimeTap = BehaviorRelay<Bool>(value: false) var tappedToShowTime: Observable<Bool> { showTimeTap.asObservable() } @@ -134,6 +140,8 @@ class MessageCell: UITableViewCell, NibReusable, PlayerDelegate { self.messageId = nil self.isCopyable = false self.couldBeShared = false + self.couldBeResend = false + self.couldBeForward = false self._deleteMessage.accept(false) if let longGestureRecognizer = longGestureRecognizer { self.bubble.removeGestureRecognizer(longGestureRecognizer) @@ -207,21 +215,17 @@ class MessageCell: UITableViewCell, NibReusable, PlayerDelegate { } } - func supportFullScreenMode() -> Bool { - return (playerView != nil && playerView!.viewModel.hasVideo.value) || self.transferImageView.image != nil - } - // MARK: Configure func configureTapGesture() { - let shownByDefault = !self.timeLabel.isHidden && !showTimeTap.value && !supportFullScreenMode() + let shownByDefault = !self.timeLabel.isHidden && !showTimeTap.value && !self.couldBeShared if shownByDefault { return } self.bubble.isUserInteractionEnabled = true self.tapGestureRecognizer = UITapGestureRecognizer() self.tapGestureRecognizer?.numberOfTapsRequired = 1 self.tapGestureRecognizer?.delegate = self self.tapGestureRecognizer!.rx.event.bind(onNext: { [weak self] _ in self?.onTapGesture() }).disposed(by: self.disposeBag) - self.tapGestureRecognizer!.cancelsTouchesInView = supportFullScreenMode() ? true : false + self.tapGestureRecognizer!.cancelsTouchesInView = self.couldBeShared ? true : false self.bubble.addGestureRecognizer(tapGestureRecognizer!) guard let doubleTap = doubleTapGestureRecognizer else { return } self.tapGestureRecognizer?.require(toFail: doubleTap) @@ -245,7 +249,7 @@ class MessageCell: UITableViewCell, NibReusable, PlayerDelegate { func onTapGesture() { // for player or image expand size on tap, for other messages show time - if supportFullScreenMode() { + if self.couldBeShared { openPreview.accept(true) return } @@ -261,10 +265,13 @@ class MessageCell: UITableViewCell, NibReusable, PlayerDelegate { self.showTimeTap.accept(true) } - private func configureLongGesture(_ messageId: Int64, _ bubblePosition: BubblePosition, _ isTransfer: Bool, _ isLocationSharingBubble: Bool) { + private func configureLongGesture(_ messageId: Int64, _ bubblePosition: BubblePosition, _ isTransfer: Bool, _ isLocationSharingBubble: Bool, isTransferSuccess: Bool, isError: Bool) { self.messageId = messageId self.isCopyable = bubblePosition != .generated && !isTransfer && !isLocationSharingBubble - self.couldBeShared = bubblePosition != .generated && !isLocationSharingBubble + self.couldBeShared = isTransfer && isTransferSuccess + self.couldBeForward = bubblePosition != .generated && !isLocationSharingBubble && (isTransferSuccess || !isTransfer) + self.couldBeResend = isError && bubblePosition == .sent + self.couldBeSaved = self.couldBeShared && self.transferedImage != nil self.bubble.isUserInteractionEnabled = true longGestureRecognizer = UILongPressGestureRecognizer() longGestureRecognizer!.rx.event.bind(onNext: { [weak self] _ in self?.onLongGesture() }).disposed(by: self.disposeBag) @@ -275,7 +282,11 @@ class MessageCell: UITableViewCell, NibReusable, PlayerDelegate { becomeFirstResponder() let menu = UIMenuController.shared let shareItem = UIMenuItem(title: L10n.Global.share, action: NSSelectorFromString("share")) - menu.menuItems = [shareItem] + let forwardItem = UIMenuItem(title: L10n.Global.forward, action: NSSelectorFromString("forward")) + let saveItem = UIMenuItem(title: L10n.Global.save, action: NSSelectorFromString("save")) + let resendItem = UIMenuItem(title: L10n.Global.resend, action: NSSelectorFromString("resend")) + let previewItem = UIMenuItem(title: L10n.Global.preview, action: NSSelectorFromString("preview")) + menu.menuItems = [previewItem, shareItem, forwardItem, saveItem, resendItem] if !menu.isMenuVisible { menu.setTargetRect(self.bubble.frame, in: self) menu.setMenuVisible(true, animated: true) @@ -287,6 +298,42 @@ class MessageCell: UITableViewCell, NibReusable, PlayerDelegate { shareMessage.onNext(true) } + @objc + func forward() { + forwardMessage.onNext(true) + } + + @objc + func save() { + saveMessage.onNext(true) + } + + @objc + func resend() { + resendMessage.onNext(true) + } + + @objc + func preview() { + previewMessage.onNext(true) + } + + func deleteFile() { + _deleteMessage.accept(true) + } + + func shareFile() { + shareMessage.onNext(true) + } + + func forwardFile() { + forwardMessage.onNext(true) + } + + func saveFile() { + saveMessage.onNext(true) + } + override func copy(_ sender: Any?) { UIPasteboard.general.string = self.messageLabel?.text UIMenuController.shared.setMenuVisible(false, animated: true) @@ -302,7 +349,12 @@ class MessageCell: UITableViewCell, NibReusable, PlayerDelegate { override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { return action == #selector(UIResponderStandardEditActions.copy) && self.isCopyable || - action == #selector(UIResponderStandardEditActions.delete) || (action == NSSelectorFromString("share") && self.couldBeShared) + action == #selector(UIResponderStandardEditActions.delete) || + (action == NSSelectorFromString("forward") && self.couldBeForward) || + (action == NSSelectorFromString("share") && self.couldBeShared) || + (action == NSSelectorFromString("resend") && self.couldBeResend) || + (action == NSSelectorFromString("save") && self.couldBeSaved) || + (action == NSSelectorFromString("preview") && self.couldBeShared) } func toggleCellTimeLabelVisibility() { @@ -441,7 +493,6 @@ class MessageCell: UITableViewCell, NibReusable, PlayerDelegate { self.configureCellTimeLabel(item) self.prepareForReuseLongGesture() - self.configureLongGesture(item.message.messageId, item.bubblePosition(), item.isTransfer, item.isLocationSharingBubble) self.prepareForReuseTapGesture() @@ -522,7 +573,16 @@ class MessageCell: UITableViewCell, NibReusable, PlayerDelegate { } else if item.isTransfer { self.messageLabelMarginConstraint.constant = -2 } - + var isError = false + if item.isTransfer { + isError = item.initialTransferStatus == .error || item.initialTransferStatus == .canceled + } else if item.isText { + isError = item.message.status == .failure + } + self.configureLongGesture(item.message.messageId, item.bubblePosition(), + item.isTransfer, item.isLocationSharingBubble, + isTransferSuccess: item.initialTransferStatus == .success, + isError: isError) self.configureTapGesture() } diff --git a/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.swift b/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.swift index e55a3dd70..efc268fd3 100644 --- a/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.swift +++ b/Ring/Ring/Features/Conversations/Conversation/ConversationViewController.swift @@ -34,7 +34,9 @@ import MobileCoreServices // swiftlint:disable type_body_length class ConversationViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate, - UIDocumentPickerDelegate, StoryboardBased, ViewModelBased, MessageAccessoryViewDelegate, ContactPickerDelegate, PHPickerViewControllerDelegate { + UIDocumentPickerDelegate, StoryboardBased, ViewModelBased, + MessageAccessoryViewDelegate, ContactPickerDelegate, + PHPickerViewControllerDelegate, UIDocumentInteractionControllerDelegate { let log = SwiftyBeaver.self @@ -73,18 +75,6 @@ class ConversationViewController: UIViewController, self.setupUI() self.setupTableView() self.setupBindings() - NotificationCenter.default.rx - .notification(UIDevice.orientationDidChangeNotification) - .observeOn(MainScheduler.instance) - .subscribe(onNext: {[weak self](_) in - guard let self = self, - UIDevice.current.portraitOrLandscape else { return } - self.setupNavTitle(profileImageData: self.viewModel.profileImageData.value, - displayName: self.viewModel.displayName.value, - username: self.viewModel.userName.value) - self.tableView.reloadData() - }) - .disposed(by: self.disposeBag) /* Register to keyboard notifications to adjust tableView insets when the keybaord appears @@ -100,6 +90,22 @@ class ConversationViewController: UIViewController, keyboardDismissTapRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard)) } + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + // Waiting for screen size change + DispatchQueue.global(qos: .background).async { + sleep(UInt32(0.5)) + DispatchQueue.main.async { [weak self] in + guard let self = self, + UIDevice.current.portraitOrLandscape else { return } + self.setupNavTitle(profileImageData: self.viewModel.profileImageData.value, + displayName: self.viewModel.displayName.value, + username: self.viewModel.userName.value) + self.tableView.reloadData() + } + } + super.viewWillTransition(to: size, with: coordinator) + } + @objc private func applicationWillResignActive() { self.viewModel.setIsComposingMsg(isComposing: false) @@ -137,6 +143,8 @@ class ConversationViewController: UIViewController, self.present(alert, animated: true, completion: nil) } + // MARK: photo library + func checkPhotoLibraryPermission() { let status = PHPhotoLibrary.authorizationStatus() switch status { @@ -367,6 +375,19 @@ class ConversationViewController: UIViewController, }) } + func saveImageToGalery (image: UIImage) { + UIImageWriteToSavedPhotosAlbum(image, self, #selector(image(_:didFinishSavingWithError:contextInfo:)), nil) + } + + @objc + func image(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) { + if let error = error { + let allert = UIAlertController(title: L10n.Conversation.errorSavingImage, message: error.localizedDescription, preferredStyle: .alert) + allert.addAction(UIAlertAction(title: "OK", style: .default)) + present(allert, animated: true) + } + } + @objc func dismissKeyboard() { self.becomeFirstResponder() @@ -954,18 +975,38 @@ class ConversationViewController: UIViewController, .disposed(by: cell.disposeBag) } + // MARK: open file + + func openDocument(messageModel: MessageViewModel) { + let conversation = self.viewModel.conversation.value.conversationId + let accountId = self.viewModel.conversation.value.accountId + guard let url = messageModel.transferedFile(conversationID: conversation, accountId: accountId), + FileManager().fileExists(atPath: url.path) else { return } + let interactionController = UIDocumentInteractionController(url: url) + interactionController.delegate = self + interactionController.presentPreview(animated: true) + } + + func documentInteractionControllerViewControllerForPreview(_ controller: UIDocumentInteractionController) -> UIViewController { + if let navigationController = self.navigationController { + return navigationController + } + return self + } + + // MARK: open share menu func showShareMenu(messageModel: MessageViewModel) { let conversation = self.viewModel.conversation.value.conversationId let accountId = self.viewModel.conversation.value.accountId if let file = messageModel.transferedFile(conversationID: conversation, accountId: accountId) { - self.presentActivitycontrollerWithItems(items: [file]) + self.presentActivityControllerWithItems(items: [file]) return } if messageModel .getURLFromPhotoLibrary(conversationID: conversation, completionHandler: { [weak self, weak messageModel] url in if let url = url { - self?.presentActivitycontrollerWithItems(items: [url]) + self?.presentActivityControllerWithItems(items: [url]) return } guard let messageModel = messageModel else { return } @@ -980,16 +1021,15 @@ class ConversationViewController: UIViewController, if let image = messageModel.getTransferedImage(maxSize: 250, conversationID: conversationId, accountId: accountId) { - self.presentActivitycontrollerWithItems(items: [image]) + self.presentActivityControllerWithItems(items: [image]) } } - func presentActivitycontrollerWithItems(items: [Any]) { + func presentActivityControllerWithItems(items: [Any]) { let activityViewController = UIActivityViewController(activityItems: items, applicationActivities: nil) activityViewController.popoverPresentationController?.sourceView = self.view activityViewController.popoverPresentationController?.permittedArrowDirections = UIPopoverArrowDirection() activityViewController.popoverPresentationController?.sourceRect = CGRect(x: self.view.bounds.midX, y: self.view.bounds.maxX, width: 0, height: 0) - activityViewController.excludedActivityTypes = [UIActivity.ActivityType.airDrop] self.present(activityViewController, animated: true, completion: nil) } @@ -1001,8 +1041,8 @@ class ConversationViewController: UIViewController, let screenSize = UIScreen.main.bounds let screenWidth = screenSize.width let screenHeight = screenSize.height - let newFrame = CGRect(x: 0, y: -statusBarHeight, width: screenWidth, height: screenHeight) - let initialFrame = CGRect(x: 0, y: screenHeight, width: screenWidth, height: screenHeight) + let newFrame = CGRect(x: 0, y: -statusBarHeight, width: screenWidth, height: screenHeight + statusBarHeight) + let initialFrame = CGRect(x: 0, y: screenHeight, width: screenWidth, height: screenHeight + statusBarHeight) contactPickerVC.view.frame = initialFrame self.view.addSubview(contactPickerVC.view) contactPickerVC.didMove(toParent: self) @@ -1063,7 +1103,7 @@ extension ConversationViewController: UITableViewDataSource { self.transferCellSetup(item, cell, tableView, indexPath) self.locationCellSetup(item, cell) self.deleteCellSetup(cell) - self.shareMessageCellSeUp(cell, item: item) + self.messageCellActionsSetUp(cell, item: item) self.tapToShowTimeCellSetup(cell) return cell @@ -1089,12 +1129,47 @@ extension ConversationViewController: UITableViewDataSource { .disposed(by: cell.disposeBag) } - private func shareMessageCellSeUp(_ cell: MessageCell, item: MessageViewModel) { + private func messageCellActionsSetUp(_ cell: MessageCell, item: MessageViewModel) { cell.shareMessage + .observeOn(MainScheduler.instance) + .subscribe(onNext: { [weak self, weak item] (shouldShare) in + guard shouldShare, let item = item else { return } + self?.showShareMenu(messageModel: item) + }) + .disposed(by: cell.disposeBag) + cell.forwardMessage + .observeOn(MainScheduler.instance) + .subscribe(onNext: { [weak self, weak item] (shouldForward) in + guard shouldForward, let item = item else { return } + self?.viewModel.slectContactsToShareMessage(message: item) + }) + .disposed(by: cell.disposeBag) + cell.saveMessage + .observeOn(MainScheduler.instance) + .subscribe(onNext: { [weak self, weak cell] (shouldSave) in + guard shouldSave, let cell = cell, let image = cell.transferedImage else { return } + self?.saveImageToGalery(image: image) + }) + .disposed(by: cell.disposeBag) + cell.resendMessage .observeOn(MainScheduler.instance) .subscribe(onNext: { [weak self, weak item] (shouldResend) in guard shouldResend, let item = item else { return } - self?.viewModel.slectContactsToShareMessage(message: item) + self?.viewModel.resendMessage(message: item) + }) + .disposed(by: cell.disposeBag) + cell.previewMessage + .observeOn(MainScheduler.instance) + .subscribe(onNext: { [weak self, weak item, weak cell] (shouldPreview) in + guard shouldPreview, let item = item, let cell = cell, let self = self, let initialFrame = cell.getInitialFrame() else { return } + let player = item.getPlayer(conversationViewModel: self.viewModel) + let image = cell.transferedImage + if player == nil && image == nil { + self.openDocument(messageModel: item) + return + } + self.inputAccessoryView.isHidden = true + self.viewModel.openFullScreenPreview(parentView: self, viewModel: player, image: image, initialFrame: initialFrame, delegate: cell) }) .disposed(by: cell.disposeBag) } @@ -1196,13 +1271,16 @@ extension ConversationViewController: UITableViewDataSource { .disposed(by: cell.disposeBag) cell.openPreview .subscribe(onNext: { [weak self, weak item, weak cell] open in - guard let self = self, open, - let initialFrame = cell?.getInitialFrame() else { return } - let player = item?.getPlayer(conversationViewModel: self.viewModel) - let image = cell?.transferedImage - if player == nil && image == nil { return } + guard let self = self, open, let cell = cell, let item = item, + let initialFrame = cell.getInitialFrame() else { return } + let player = item.getPlayer(conversationViewModel: self.viewModel) + let image = cell.transferedImage + if player == nil && image == nil { + self.openDocument(messageModel: item) + return + } self.inputAccessoryView.isHidden = true - self.viewModel.openFullScreenPreview(parentView: self, viewModel: player, image: image, initialFrame: initialFrame) + self.viewModel.openFullScreenPreview(parentView: self, viewModel: player, image: image, initialFrame: initialFrame, delegate: cell) }) .disposed(by: cell.disposeBag) cell.playerHeight diff --git a/Ring/Ring/Features/Conversations/Conversation/ConversationViewModel.swift b/Ring/Ring/Features/Conversations/Conversation/ConversationViewModel.swift index 932969b54..20f8fd5e9 100644 --- a/Ring/Ring/Features/Conversations/Conversation/ConversationViewModel.swift +++ b/Ring/Ring/Features/Conversations/Conversation/ConversationViewModel.swift @@ -703,8 +703,8 @@ extension ConversationViewModel { contactUri: self.conversation.value.participantUri) } - func openFullScreenPreview(parentView: UIViewController, viewModel: PlayerViewModel?, image: UIImage?, initialFrame: CGRect) { - self.stateSubject.onNext(ConversationState.openFullScreenPreview(parentView: parentView, viewModel: viewModel, image: image, initialFrame: initialFrame)) + func openFullScreenPreview(parentView: UIViewController, viewModel: PlayerViewModel?, image: UIImage?, initialFrame: CGRect, delegate: PreviewViewControllerDelegate) { + self.stateSubject.onNext(ConversationState.openFullScreenPreview(parentView: parentView, viewModel: viewModel, image: image, initialFrame: initialFrame, delegate: delegate)) } } @@ -764,6 +764,36 @@ extension ConversationViewModel { self.changeConversationIfNeeded(items: selectedContacts) } + func resendMessage(message: MessageViewModel) { + guard !message.message.isGenerated, + !message.message.isLocationSharing else { return } + if !message.message.isTransfer { + self.sendMessage(withContent: message.content, contactURI: conversation.value.participantUri) + return + } + let conversationId = self.conversation.value.conversationId + let accountId = self.conversation.value.accountId + var fileName = message.content + if message.content.contains("\n") { + guard let substring = message.content.split(separator: "\n").first else { return } + fileName = String(substring) + } + if let url = message.transferedFile(conversationID: conversationId, accountId: accountId) { + self.sendFile(filePath: url.path, displayName: fileName, contactHash: self.conversation.value.hash) + return + } + if let image = message.getTransferedImage(maxSize: 200, conversationID: conversationId, accountId: accountId) { + let identifier = message.transferFileData.identifier + if identifier != nil { + self.sendImageFromPhotoLibraty(image: image, imageName: fileName, localIdentifier: identifier, contactHash: self.conversation.value.hash) + return + } + if let data = image.jpegData(compressionQuality: 100) { + self.sendAndSaveFile(displayName: fileName, imageData: data, contactHash: self.conversation.value.hash, conversation: self.conversation.value.conversationId) + } + } + } + func slectContactsToShareMessage(message: MessageViewModel) { guard !message.message.isGenerated, !message.message.isLocationSharing else { return } diff --git a/Ring/Ring/Features/Conversations/Conversation/MessageViewModel.swift b/Ring/Ring/Features/Conversations/Conversation/MessageViewModel.swift index c401b65f9..2ffb909e3 100644 --- a/Ring/Ring/Features/Conversations/Conversation/MessageViewModel.swift +++ b/Ring/Ring/Features/Conversations/Conversation/MessageViewModel.swift @@ -68,6 +68,7 @@ class MessageViewModel { var isComposingIndicator: Bool = false var isLocationSharingBubble: Bool { return self.message.isLocationSharing } + var isText: Bool { return !self.message.isLocationSharing && !self.message.isGenerated && !self.message.isTransfer } private let disposeBag = DisposeBag() let injectBug: InjectionBag diff --git a/Ring/Ring/Features/Conversations/Preview/PreviewViewController.storyboard b/Ring/Ring/Features/Conversations/Preview/PreviewViewController.storyboard index eebbc9187..221f9d69e 100644 --- a/Ring/Ring/Features/Conversations/Preview/PreviewViewController.storyboard +++ b/Ring/Ring/Features/Conversations/Preview/PreviewViewController.storyboard @@ -1,10 +1,12 @@ <?xml version="1.0" encoding="UTF-8"?> -<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="16097.3" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="edr-AU-dxH"> +<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="edr-AU-dxH"> <device id="retina6_1" orientation="portrait" appearance="light"/> <dependencies> <deployment identifier="iOS"/> - <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/> + <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17703"/> + <capability name="Image references" minToolsVersion="12.0"/> <capability name="Safe area layout guides" minToolsVersion="9.0"/> + <capability name="System colors in document resources" minToolsVersion="11.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> </dependencies> <scenes> @@ -23,6 +25,41 @@ <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="kNJ-67-zFf"> <rect key="frame" x="0.0" y="0.0" width="414" height="896"/> </imageView> + <stackView opaque="NO" contentMode="scaleToFill" distribution="fillEqually" alignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="3aB-Lr-pJQ"> + <rect key="frame" x="0.0" y="772" width="414" height="90"/> + <subviews> + <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Gvs-6O-5Fs"> + <rect key="frame" x="0.0" y="33" width="103.5" height="24"/> + <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> + <state key="normal"> + <imageReference key="image" image="ic_share" symbolScale="large"/> + </state> + </button> + <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="GFG-Jq-Pqx"> + <rect key="frame" x="103.5" y="31.5" width="103.5" height="27"/> + <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> + <state key="normal"> + <imageReference key="image" image="ic_forward" symbolScale="large"/> + </state> + </button> + <button opaque="NO" contentMode="center" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="5q1-cC-MI7"> + <rect key="frame" x="207" y="31.5" width="103.5" height="27"/> + <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> + <state key="normal" image="ic_save"> + <preferredSymbolConfiguration key="preferredSymbolConfiguration" scale="default"/> + </state> + </button> + <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="5pB-7e-B3j"> + <rect key="frame" x="310.5" y="31.5" width="103.5" height="27"/> + <color key="tintColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> + <state key="normal" image="ic_conversation_remove"/> + </button> + </subviews> + <color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.7987211045" colorSpace="custom" customColorSpace="sRGB"/> + <constraints> + <constraint firstAttribute="height" constant="90" id="BTB-tx-Psf"/> + </constraints> + </stackView> <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="umc-Uj-AVs"> <rect key="frame" x="0.0" y="0.0" width="414" height="80"/> <constraints> @@ -32,21 +69,25 @@ <view contentMode="scaleToFill" id="Jga-0U-qXG" customClass="PlayerView" customModule="Ring" customModuleProvider="target"> <rect key="frame" x="0.0" y="0.0" width="414" height="896"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> - <color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/> + <color key="backgroundColor" systemColor="systemBackgroundColor"/> </view> - <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="s0z-cK-ISY"> - <rect key="frame" x="340" y="44" width="49" height="35"/> + <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="s0z-cK-ISY"> + <rect key="frame" x="341" y="44" width="48" height="35"/> <fontDescription key="fontDescription" type="system" pointSize="19"/> <state key="normal" title="Close"> <color key="titleColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> </state> </button> </subviews> + <viewLayoutGuide key="safeArea" id="dJP-6X-zM4"/> <constraints> <constraint firstItem="s0z-cK-ISY" firstAttribute="top" secondItem="dJP-6X-zM4" secondAttribute="top" priority="250" id="0CU-8Q-b0S"/> + <constraint firstItem="3aB-Lr-pJQ" firstAttribute="leading" secondItem="dJP-6X-zM4" secondAttribute="leading" id="26g-Gx-ObU"/> <constraint firstItem="9Qb-6X-Utl" firstAttribute="leading" secondItem="QAC-jd-TeH" secondAttribute="leading" id="39d-qj-71g"/> <constraint firstItem="s0z-cK-ISY" firstAttribute="trailing" secondItem="Jga-0U-qXG" secondAttribute="trailing" constant="-25" id="IcL-2k-lX8"/> + <constraint firstItem="dJP-6X-zM4" firstAttribute="trailing" secondItem="3aB-Lr-pJQ" secondAttribute="trailing" id="Mj0-Nc-ouz"/> <constraint firstItem="Jga-0U-qXG" firstAttribute="top" secondItem="QAC-jd-TeH" secondAttribute="top" id="Ome-jS-TQa"/> + <constraint firstItem="dJP-6X-zM4" firstAttribute="bottom" secondItem="3aB-Lr-pJQ" secondAttribute="bottom" id="QMX-S4-F1Q"/> <constraint firstAttribute="bottom" secondItem="9Qb-6X-Utl" secondAttribute="bottom" id="R1V-fE-JGu"/> <constraint firstItem="umc-Uj-AVs" firstAttribute="top" secondItem="QAC-jd-TeH" secondAttribute="top" id="RVi-sm-LCM"/> <constraint firstItem="umc-Uj-AVs" firstAttribute="leading" secondItem="dJP-6X-zM4" secondAttribute="leading" id="XwX-7d-vQ6"/> @@ -61,12 +102,14 @@ <constraint firstItem="Jga-0U-qXG" firstAttribute="leading" secondItem="dJP-6X-zM4" secondAttribute="leading" id="lQU-Ek-RAG"/> <constraint firstItem="9Qb-6X-Utl" firstAttribute="top" secondItem="QAC-jd-TeH" secondAttribute="top" id="nIf-wb-wOx"/> </constraints> - <viewLayoutGuide key="safeArea" id="dJP-6X-zM4"/> </view> <nil key="simulatedTopBarMetrics"/> <nil key="simulatedBottomBarMetrics"/> <connections> <outlet property="backgroundView" destination="9Qb-6X-Utl" id="nva-VK-fcG"/> + <outlet property="buttonsContainer" destination="3aB-Lr-pJQ" id="u7z-BG-n68"/> + <outlet property="deleteButton" destination="5pB-7e-B3j" id="Fcj-G9-OLd"/> + <outlet property="forwardButton" destination="GFG-Jq-Pqx" id="Vot-bV-hkU"/> <outlet property="gradientView" destination="umc-Uj-AVs" id="q4m-Ib-c0V"/> <outlet property="hideButton" destination="s0z-cK-ISY" id="PeP-TI-Y2O"/> <outlet property="imageBottomConstraint" destination="Z3A-GV-6Py" id="4dp-wF-Ix2"/> @@ -75,6 +118,8 @@ <outlet property="imageTrailingConstraint" destination="Zxa-bY-zaY" id="gac-QR-lKz"/> <outlet property="imageView" destination="kNJ-67-zFf" id="rTG-bA-Gz6"/> <outlet property="playerView" destination="Jga-0U-qXG" id="nNg-vJ-wZp"/> + <outlet property="saveButton" destination="5q1-cC-MI7" id="QG0-Vq-ETD"/> + <outlet property="shareButton" destination="Gvs-6O-5Fs" id="FNx-Hm-Hav"/> </connections> </viewController> <placeholder placeholderIdentifier="IBFirstResponder" id="5Ku-db-uak" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/> @@ -82,4 +127,13 @@ <point key="canvasLocation" x="137.68115942028987" y="96.428571428571431"/> </scene> </scenes> + <resources> + <image name="ic_conversation_remove" width="27" height="27"/> + <image name="ic_forward" width="27" height="27"/> + <image name="ic_save" width="27" height="27"/> + <image name="ic_share" width="24" height="24"/> + <systemColor name="systemBackgroundColor"> + <color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> + </systemColor> + </resources> </document> diff --git a/Ring/Ring/Features/Conversations/Preview/PreviewViewController.swift b/Ring/Ring/Features/Conversations/Preview/PreviewViewController.swift index 0d55efeb2..a70802744 100644 --- a/Ring/Ring/Features/Conversations/Preview/PreviewViewController.swift +++ b/Ring/Ring/Features/Conversations/Preview/PreviewViewController.swift @@ -27,6 +27,13 @@ enum PrevewType { case image } +protocol PreviewViewControllerDelegate: class { + func deleteFile() + func shareFile() + func forwardFile() + func saveFile() +} + class PreviewViewController: UIViewController, StoryboardBased, ViewModelBased { // MARK: - outlets @IBOutlet weak var playerView: PlayerView! @@ -38,12 +45,18 @@ class PreviewViewController: UIViewController, StoryboardBased, ViewModelBased { @IBOutlet weak var imageBottomConstraint: NSLayoutConstraint! @IBOutlet weak var backgroundView: UIView! @IBOutlet weak var gradientView: UIView! +@IBOutlet weak var shareButton: UIButton! +@IBOutlet weak var deleteButton: UIButton! +@IBOutlet weak var forwardButton: UIButton! +@IBOutlet weak var saveButton: UIButton! +@IBOutlet weak var buttonsContainer: UIStackView! // MARK: - members let disposeBag = DisposeBag() var viewModel: PreviewControllerModel! var tapGestureRecognizer: UITapGestureRecognizer! var type: PrevewType = .player + weak var delegate: PreviewViewControllerDelegate? override func viewDidLoad() { super.viewDidLoad() @@ -67,8 +80,41 @@ class PreviewViewController: UIViewController, StoryboardBased, ViewModelBased { .disposed(by: self.disposeBag) self.hideButton.centerYAnchor.constraint(equalTo: self.playerView.muteAudio.centerYAnchor, constant: 0).isActive = true self.hideButton.setTitle(L10n.Global.close, for: .normal) + self.shareButton.isUserInteractionEnabled = self.type == .image + self.deleteButton.isUserInteractionEnabled = self.type == .image + self.forwardButton.isUserInteractionEnabled = self.type == .image + buttonsContainer.isHidden = self.type != .image if self.type == .image, let image = self.viewModel.image { self.imageView.image = image + let pinchGesture = UIPinchGestureRecognizer(target: self, action: #selector(startZooming(_:))) + imageView.isUserInteractionEnabled = true + imageView.addGestureRecognizer(pinchGesture) + self.shareButton.rx.tap + .subscribe(onNext: { [weak self] in + self?.share() + }) + .disposed(by: self.disposeBag) + self.deleteButton.rx.tap + .subscribe(onNext: { [weak self] in + self?.parent?.inputAccessoryView?.isHidden = false + self?.removeChildController() + self?.delete() + }) + .disposed(by: self.disposeBag) + self.forwardButton.rx.tap + .subscribe(onNext: { [weak self] in + self?.forward() + self?.parent?.inputAccessoryView?.isHidden = false + self?.removeChildController() + }) + .disposed(by: self.disposeBag) + self.saveButton.rx.tap + .subscribe(onNext: { [weak self] in + self?.parent?.inputAccessoryView?.isHidden = false + self?.removeChildController() + self?.save() + }) + .disposed(by: self.disposeBag) return } guard let model = self.viewModel.playerViewModel, let playerView = playerView else { return } @@ -77,6 +123,14 @@ class PreviewViewController: UIViewController, StoryboardBased, ViewModelBased { self.view.addGestureRecognizer(tapGestureRecognizer) } + @objc + private func startZooming(_ sender: UIPinchGestureRecognizer) { + let scaleResult = sender.view?.transform.scaledBy(x: sender.scale, y: sender.scale) + guard let scale = scaleResult, scale.a > 1, scale.d > 1 else { return } + sender.view?.transform = scale + sender.scale = 1 + } + override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) self.navigationController?.setNavigationBarHidden(true, animated: animated) @@ -120,4 +174,25 @@ class PreviewViewController: UIViewController, StoryboardBased, ViewModelBased { self.view.layoutIfNeeded() }, completion: nil) } + func share() { + if let delegate = self.delegate { + delegate.shareFile() + } + } + func delete() { + if let delegate = self.delegate { + delegate.deleteFile() + } + } + func forward() { + if let delegate = self.delegate { + delegate.forwardFile() + } + } + + func save() { + if let delegate = self.delegate { + delegate.saveFile() + } + } } diff --git a/Ring/Ring/Protocols/ConversationNavigation.swift b/Ring/Ring/Protocols/ConversationNavigation.swift index 22c741bb2..f01bda3cc 100644 --- a/Ring/Ring/Protocols/ConversationNavigation.swift +++ b/Ring/Ring/Protocols/ConversationNavigation.swift @@ -36,7 +36,7 @@ enum ConversationState: State { case fromCallToConversation(conversation: ConversationViewModel) case needAccountMigration(accountId: String) case accountModeChanged - case openFullScreenPreview(parentView: UIViewController, viewModel: PlayerViewModel?, image: UIImage?, initialFrame: CGRect) + case openFullScreenPreview(parentView: UIViewController, viewModel: PlayerViewModel?, image: UIImage?, initialFrame: CGRect, delegate: PreviewViewControllerDelegate) case replaceCurrentWithConversationFor(participantUri: String) } @@ -75,8 +75,8 @@ extension ConversationNavigation where Self: Coordinator, Self: StateableRespons self.migrateAccount(accountId: accountId) case .accountModeChanged: self.accountModeChanged() - case .openFullScreenPreview(let parentView, let viewModel, let image, let initialFrame): - self.openFullScreenPreview(parentView: parentView, viewModel: viewModel, image: image, initialFrame: initialFrame) + case .openFullScreenPreview(let parentView, let viewModel, let image, let initialFrame, let delegate): + self.openFullScreenPreview(parentView: parentView, viewModel: viewModel, image: image, initialFrame: initialFrame, delegate: delegate) default: break } @@ -105,9 +105,10 @@ extension ConversationNavigation where Self: Coordinator, Self: StateableRespons withStateable: recordFileViewController.viewModel) } - func openFullScreenPreview(parentView: UIViewController, viewModel: PlayerViewModel?, image: UIImage?, initialFrame: CGRect) { + func openFullScreenPreview(parentView: UIViewController, viewModel: PlayerViewModel?, image: UIImage?, initialFrame: CGRect, delegate: PreviewViewControllerDelegate) { if viewModel == nil && image == nil { return } let previewController = PreviewViewController.instantiate(with: self.injectionBag) + previewController.delegate = delegate if let viewModel = viewModel { previewController.viewModel.playerViewModel = viewModel previewController.type = .player diff --git a/Ring/Ring/Resources/Images.xcassets/Contents.json b/Ring/Ring/Resources/Images.xcassets/Contents.json index 37c3608ab..18a5a36fd 100644 --- a/Ring/Ring/Resources/Images.xcassets/Contents.json +++ b/Ring/Ring/Resources/Images.xcassets/Contents.json @@ -1,9 +1,9 @@ { "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 }, "properties" : { "compression-type" : "lossy" } -} \ No newline at end of file +} diff --git a/Ring/Ring/Resources/Images.xcassets/ic_conversation_remove.imageset/Contents.json b/Ring/Ring/Resources/Images.xcassets/ic_conversation_remove.imageset/Contents.json index e73088e1e..07738c2f4 100644 --- a/Ring/Ring/Resources/Images.xcassets/ic_conversation_remove.imageset/Contents.json +++ b/Ring/Ring/Resources/Images.xcassets/ic_conversation_remove.imageset/Contents.json @@ -1,23 +1,27 @@ { "images" : [ { + "filename" : "trash-can-outline.png", "idiom" : "universal", - "filename" : "delete-2.png", "scale" : "1x" }, { + "filename" : "trash-can-outline-2.png", "idiom" : "universal", - "filename" : "delete.png", "scale" : "2x" }, { + "filename" : "trash-can-outline-3.png", "idiom" : "universal", - "filename" : "delete-1.png", "scale" : "3x" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" } -} \ No newline at end of file +} diff --git a/Ring/Ring/Resources/Images.xcassets/ic_conversation_remove.imageset/delete-1.png b/Ring/Ring/Resources/Images.xcassets/ic_conversation_remove.imageset/delete-1.png deleted file mode 100644 index 04d7a8edcbfb56087078e6509c0e8f39b3ac2461..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 391 zcmeAS@N?(olHy`uVBq!ia0y~yVDJWE4mJh`1`EHcR}2gcY)RhkE)4%caKYZ?lNlHo zZ9H8ZLn`LHy=~}s*g(eZWB5Cko-->7WE@13k8X5GTJlJ3DR=6!*ubL?&U?O5xaa>X ze`}JxX{eEiisz&*U90&2Tss)faoPRm&1tyqS#A>i{lLPccU3vtma}eHtaq*NU%AVZ ziN6kio5!DES~uywuC&aGqcO|b^?xSXmd0&N>(jf;YP!)(H2c|$wO3oi4ov&EJ?Lys zWBbR+9+Up9S-76JJ+SOz*w>la^EUs}O=j&8H3&Np!AMPpgw*ToDY08VTc&)x5b>V* z`u1z3`?r7j|3AU#bf(di>P7V}lOp{^6-t@+FjW6?|7ltMV5VfvVinIxDqG7~ZpIt? VTkC24WME)m@O1TaS?83{1ORG*pIiU{ diff --git a/Ring/Ring/Resources/Images.xcassets/ic_conversation_remove.imageset/delete-2.png b/Ring/Ring/Resources/Images.xcassets/ic_conversation_remove.imageset/delete-2.png deleted file mode 100644 index 8e741fc4248e814b22b74fb58cc2891b3e4ea0da..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 272 zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4mJh`hT^KKFANL}Y)RhkE)4%caKYZ?lNlHo zj(WN{hD5Z!y=Iu@6e!U8@caS~F2_xcItMSAEp<J>yQ6=NqW1=W!8b>ag{=wN#bTZ? z*~vw5<)xP2^L|NeDB!!3rk8&1{N7-mmC~!P9{AkBZ}We3zTrVTrU$I~8RGA%B*K2| z`w+WA{$t<Or3t}*SmtMHu6V^d&%o_@&?953q`ugI{T5ewA96G=n8Q2m`YfgA3x53n zP@o&SMM!}WiWcxa5SbBtb2VRrEJwgj-s#ug+1D=W4=<5slizS)%|iQ9_Ft^aq;1@8 U)^L_FFfcH9y85}Sb4q9e0GK3g%>V!Z diff --git a/Ring/Ring/Resources/Images.xcassets/ic_conversation_remove.imageset/delete.png b/Ring/Ring/Resources/Images.xcassets/ic_conversation_remove.imageset/delete.png deleted file mode 100644 index d182d87146a01e56fc1aff7eb7ef4676852001e8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 306 zcmeAS@N?(olHy`uVBq!ia0y~yU@!t<4mJh`208nVjSLJ7Y)RhkE)4%caKYZ?lNlHo zo_o4DhGaCpy}6g~V1h{dNA~8{*&A+lTrHJot$xsz!^W0(I43jb5!bAUCDWvC&)fI6 z;Kcvwdu|6lzqrJ?vBP75t^l*=vCc-{yYaUS&z)_USXjpXg*$EDflC#8r3(ztJ&T(B z_w}~YFjwy~Ud7_WK9{p@CUUvdyw7tvu%u65BEz-g`Um!H)cvHZ<DBX=>457hqqiBA zfBvzwRxB3C7kSj{v?s8`oa>{T;ysNc(g;CK;ZGSk-8tp26J}0uKA;pTJA20q0gKD` z<ZaXw5@Oe#@qW`^SXIS1e`^Kf^)DSAe^#2W`B%7h@5)W`3=9kmp00i_>zopr06cJp A;{X5v diff --git a/Ring/Ring/Resources/Images.xcassets/ic_conversation_remove.imageset/trash-can-outline-2.png b/Ring/Ring/Resources/Images.xcassets/ic_conversation_remove.imageset/trash-can-outline-2.png new file mode 100644 index 0000000000000000000000000000000000000000..1a3e09cebd4b5665f22a9792bb403e40986e8d56 GIT binary patch literal 562 zcmeAS@N?(olHy`uVBq!ia0y~yU@!w=4mJh`h91|fy9^8rEa{HEjtmSN`?>!lvNA9* zC?tCX`7$t6sWC7#v@kIIVqjosc)`F>YQVtoDuIE)Y6b&?c)^@qfi?^b3~Wi>?k)^q z@Y8vBJp%&+XMsm#F#`ib0vLbDb~?|%z-Z#>;uw-~@9iu@zas_$uKN?US)8gF^&A`z zitb?fe#5(feT5~X=uxh~;37sVmyL^VCe3zlW|3CRl}Tzh{BIhlq&_dbKQ={2GP&oc zQtw~K2QimyK5q^6dYSp^;0@JnD)XP-D175<E3ov%)Gmf#_FbYSUe=HE%8u!X^+ujA z3!i`8+f)7iy!{@#bVZ`~?yI|T%KiSb<F8iV4A~N4;wJujvsu=f(DM-{I%2%tu7{H@ zZa`tq5`F#qut|ICw%5t48*~3ni<r+8*Q*r2dF72Y5qVl=+zrJK76s(owJHnyXc+!x zZE?NY%Y73freB|PZ0E{~aL+|~_Yd$?$P{Qj*lxq{+qIq7f+?@Tmvtd;0k6!nW0pS{ zxC?Y%hFo%7Xc*&N<)^t<{MSCiV}F$G80KD>apUM+!&(Lg2GtVRh?11Vl2ohYqSVBa zR0bmhBST#S6I~;t5JOWdV@oSjb8Q0yD+7a*7p5AZXvob^$xN%ntzk#6ZVxE_JYD@< J);T3K0RU*8%jW<9 literal 0 HcmV?d00001 diff --git a/Ring/Ring/Resources/Images.xcassets/ic_conversation_remove.imageset/trash-can-outline-3.png b/Ring/Ring/Resources/Images.xcassets/ic_conversation_remove.imageset/trash-can-outline-3.png new file mode 100644 index 0000000000000000000000000000000000000000..4dc5af188f782b8018e9aa8d4da19daf5b200415 GIT binary patch literal 769 zcmeAS@N?(olHy`uVBq!ia0y~yU<d?Z4mJh`hE@GuW(*7rEa{HEjtmSN`?>!lvNA9* zC?tCX`7$t6sWC7#v@kIIVqjosc)`F>YQVtoDuIE)Y6b&?c)^@qfi?^b3~Wi>?k)^q z@Y8vBJp%&+XMsm#F#`ib0vLbDb~?|%z{Kq7;uunK>+PM5-cti*j(vRZFVnqjQifo; z`thi!KkQM`t!)`=9Hi#Fw&!qa3cKDiO=gk;$1LePTv}3(9;w;!N;Q4C_raF``wW}; zp6^yaw|#!PcJKSW1^g~xFem)#>m!rqF_rf?=Y?KfESEj=ec`3#@J7|o6|rZ&T77gi zdNotyrC7n{k9ST#+p6PTb;sgZUg%!q^RM5l<i1F{C2?7K`esKZDV0TF;^2yMok`Oy zb_T{V#PR-lCH)}sy(<%c<JHFG^2Yq*_U}Wl-aC`7o>_QG_i)<sjOEXFSf=)MuYLTy z$jrl~*~J7*ybvoqqy4OMukZ=uiwc6gLX;B=o^SVwXL`@x_jk^xAA1{<=U3}4i9WLG z7Gv4DALhajD%V@`pM07#p@Xe6158}lBi*?qdrpn{yDgj_7JpZ=yp{6!(}9hxtA5=x z+M)bN<^%T*liy}%)ecNu6?9tR?DXPLS*F{2vwObmGF6yiCO&BkQ_X|_zvQbq)`VPL zeyvfbd)=!31C!4Mp3Ax`c){-yv*+)<-&23K*Lr@R`+D1ZQMH$B8`$o!t-Ji)xzC=j zVf}&V2d<&DFKrigfWgflyv$1WLcxuT-5D4dR7+eVN>UO_QmvAUQWHy38H@~!40R1m zbd8Kc3{9;}46F<+wG9lc3=CKl?>V4o$jwj5OsmALp|mFL4=6c$y85}Sb4q9e0K$JR AY5)KL literal 0 HcmV?d00001 diff --git a/Ring/Ring/Resources/Images.xcassets/ic_conversation_remove.imageset/trash-can-outline.png b/Ring/Ring/Resources/Images.xcassets/ic_conversation_remove.imageset/trash-can-outline.png new file mode 100644 index 0000000000000000000000000000000000000000..6a02c464c273daa50ac11ff3c51d51ca0d56ccf2 GIT binary patch literal 481 zcmeAS@N?(olHy`uVBq!ia0y~yV2}o34mJh`hTbb*LKzqsSkfJR9T^xl_H+M9WMyDr zP)PO&@?~JCQe$9fXklRZ#lXPO@PdJ%)PRBERRRNp)eHs(@q#(K0&N%=7}%1$-CY>K z;HUHMdIkmt&H|6fVg?3=1Tg-P?R1`jf#I>Ii(`n#@wZcL`5YZZTK8*nO?7Ee)yQc) z5@sTl9kB9^V-mNSVvgfNg^Q=UI|N#vWVku1ov~RTa3)i&xc=PE=VszUTn(*aUb7fV z{k4oY2XeUIbiT!EHSd;Uz~>|Vll<oWICW;)eVLw6o@4QI8KV<;w=_zBsN5A^(%AL- zK3l2BtKBcl1f4u??cBFdKfX)xx9Gv7_rcE#|Ia@+DMH9QX=h;Hinlv|HUB=}$XcKu zbLPyq{Qt%UdWUXa-COE;nPcB`)%`5x9$8+eisa7v1RdTzm%m9jfgwbB&fJEXGjo%! z%@$~7|9AI6(e-9F83qOh)e_f;l9a@fRIB8o)Wnih1|tI_LtO(CT_d9qLsKhbODhvg nZ36=<1A|c4cb`!-<mRVjrd8tBpdu0G4+?HiS3j3^P6<r_mguPQ literal 0 HcmV?d00001 diff --git a/Ring/Ring/Resources/Images.xcassets/ic_forward.imageset/Contents.json b/Ring/Ring/Resources/Images.xcassets/ic_forward.imageset/Contents.json new file mode 100644 index 000000000..ac290e18f --- /dev/null +++ b/Ring/Ring/Resources/Images.xcassets/ic_forward.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "share-outline.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "share-outline-2.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "share-outline-4.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Ring/Ring/Resources/Images.xcassets/ic_forward.imageset/share-outline-2.png b/Ring/Ring/Resources/Images.xcassets/ic_forward.imageset/share-outline-2.png new file mode 100644 index 0000000000000000000000000000000000000000..de5588ac43f6f007ac0ed86875cf637bfe253a30 GIT binary patch literal 834 zcmeAS@N?(olHy`uVBq!ia0y~yU@!w=4mJh`h91|fy9^8rEa{HEjtmSN`?>!lvNA9* zC?tCX`7$t6sWC7#v@kIIVqjosc)`F>YQVtoDuIE)Y6b&?c)^@qfi?^b3~Wi>?k)^q z@Y8vBJp%&+XMsm#F#`ib0vLbDb~?|%z~tiT;uw-~@9oUJ*&>b-?e)S>uH1<DF_EJ& zB12iIg_&Eh$<=51LA5BBKm2QQ_oykXi0DvQWG1Au#e>7;G5a#UC;T2wU71@1OudRe zy2y1$8^~<Gx%cMHyM||&Pm7j+-uHRW<}-UYYqK&nIw%Nmu>4DJa7+l7+qm1SOnQn5 z>zjLdNs~-eGB0hN!=JSC#80JXG3V3`mly5pw4D4o@J#2N^+_|I`b;!t?OF18(yB>p zD!krSvkcy}E)h&V<2JF_S2>u`t>O5A^fwjr!rPOSudv@?+gGCT@qoAG0`?vJe>TO; z)m#5$5oavpzU3l6n01&ZA1HlbR8SPW=F`)LxCf<NGb<lR6-d4C3taK(NrTphOzxSX z57Y{dC~ez%r>`gI@T7U3w=O?>-L}Mf(y?B{`$cQlc{dn4*e;vJePCnH$L{^2jf@Y{ zeJ-gVIQOEwz2WnL&<Bbi#3ESinBp7VUOzBe`i=2@@}zsrdW^ycxXPw%&b)AE-tCXp zyEX-NU769`dHce~kXLFFH?%W7FV%I=TH{%z{<3+=t|jsp(?c(<Y}xqJ?BId5Q?9OM zE_UdBcJAh8*E6$TPkj}#YWk10va{Y^)PA5OJ7>Po5w4edc}G<88Oj^$PVd-iv{Y-0 zeU%OCB)(3wq@yM~5427@viiV^#W&U_ZEp2&J-OUmbsjsPbx*8z&C*3j*nfUsZtZ<2 zP`P<V@yaF3L(;F>FrM+SO7Dp*kGAr><SqJl16u{l9!4$8qvzk61_eE`Jh1HY2k{3C z1&iKPvVi0ChT9)jZCCAr-&-H-VPIfTEpd$~Nl7e8wMs5ZO)N=eFfuSQ)HN{CH8Kh@ vG_^9eurf5#HZZU<F!+#gKNv+rZhlH;S|x4`jkf9ULFw7k)z4*}Q$iB}tTtF0 literal 0 HcmV?d00001 diff --git a/Ring/Ring/Resources/Images.xcassets/ic_forward.imageset/share-outline-4.png b/Ring/Ring/Resources/Images.xcassets/ic_forward.imageset/share-outline-4.png new file mode 100644 index 0000000000000000000000000000000000000000..618f70b96585daa2a263596b9d262b1a9e049bfc GIT binary patch literal 1188 zcmeAS@N?(olHy`uVBq!ia0y~yU<d?Z4mJh`hE@GuW(*7rEa{HEjtmSN`?>!lvNA9* zC?tCX`7$t6sWC7#v@kIIVqjosc)`F>YQVtoDuIE)Y6b&?c)^@qfi?^b3~Wi>?k)^q z@Y8vBJp%&+XMsm#F#`ib0vLbDb~?|%z`V@U#WAGf*4x>>8M2`=?&o_KZWDdk&Zx<; zYjRUYr?lvwh!xxyUwQ2+RPADm*}3X2$0K(^*YMbgkTtF%F+W+AYy!lSE^w_ASrx*{ zeAHpVF|%tj;>O*IUsR1}&MZ$?`+gw$>E50DtDn{1|MUI~w~A1wi_%071a(BrVQ$x4 z#s$KPSs%KpLf;6~POo72p|tJ9KJOO34w)vG4xJ(mA;m=}6g^u!l-kt<l{#b=xy;yb zVxd<HXP3+-ml+dIEc9;S?6|`vv@msIw5!q<jUuaWXI3qW<G#ajhb1Qd<74)Q^H-~# zIJ$jPxi&7%Y1t*e!@T}3+nc-x8}2jrcD<2#wdU8XU$ftSoV&~XTg+^+?cTLfe>W<6 zzR#DO_^RjEnq4;vmc}mI7yYPq`I`g1ch8=>J@K^l^$j+mpADC>M^Aj!{7RqK%6fI( z(tU~FrWV8}u1V|+DPJvrWkPA--0=RD#joa0i|A?=TzPNRzg?V#-IcAjOWv(CtlRIp zs(<zWFoEr>>Q>2xY&V^;xL)b%0=sy}d-bn4zqW}P=Z62+nE3SVjeh~luWZ-ikXgA| zH~p2r>NC-bHB0uWZhm_2mDm-r-^aG)Iqc!F34Bz0pP}7}BmLUUKV}cC9!MpyZxFu0 zn!~<M;`>g4Ll+MjE^C~8;IQ<Qm&fKs)ow~*l?&I`_q&pF-e&s$+$Hl)?pN2|{A|bH zh(nbRZeN`$)Hwg7dU^Y=Ns){9?2(Lo?fGW$f!bG2od@DqzrXn>u2T1}&<yWCD^fK> zXU;g_wyxvV;sYOVsBcoTXDDwM__wNh!7ZVys~hK^SU00~;l<Sj!n)_Uw<v$`e^Bx- zieXv<_W{cX`3bBRPT$rn<?7vTxK%4~ujG$uRsOuw8!I=>E0p^oX(8(rp=bDyW5?=4 z5o^CRmTG@=HB<jk{=oTx@q^O>hkmN5o3`bdTynh<tre&HKk&ZEYhLNfd%4#pOkZf7 zduA5rl-$g7tK6^56f>GK@kV6h@3ZfIu01}fDq`=ZqcYN-Z<Zaf`SbcUOMK}4!0)~a z$5*UpDz~aTy={S=!9ktJD)Tm-?svTEevIXR%BA$Wo+IzV5)^+NpB}OAyyV09BYKJ( z#hJdZ5a(K`zpCH$Oz@N&Z&hPBVq;D{zxrGGx#ZVnOxz3opDhf((ky$z_?0i~v&Q9T zq@IU<4R~Wyxh%XoxUurV%{^TF$Ik_wxUzn}_iF!@>PgRED21MT!ebzAU9f)RvHni9 zTw?lD-saKQd+YX2TE)P?pjzS@QIe8al4_M)l$uzQ%3x$*WT<OkqHAOnVrXh*Y++?& oqHSPcWnds{|F#Q7LvDUbW?Cg~4LM1npkjx?)78&qol`;+0CjH>L;wH) literal 0 HcmV?d00001 diff --git a/Ring/Ring/Resources/Images.xcassets/ic_forward.imageset/share-outline.png b/Ring/Ring/Resources/Images.xcassets/ic_forward.imageset/share-outline.png new file mode 100644 index 0000000000000000000000000000000000000000..7850257c550347ae850dcdd76efd3fc1daac3362 GIT binary patch literal 547 zcmeAS@N?(olHy`uVBq!ia0y~yV2}o34mJh`hTbb*LKzqsSkfJR9T^xl_H+M9WMyDr zP)PO&@?~JCQe$9fXklRZ#lXPO@PdJ%)PRBERRRNp)eHs(@q#(K0&N%=7}%1$-CY>K z;HUHMdIkmt&H|6fVg?3=1Tg-P?R1`jfl<}d#WBR=_}i%qy;&S(-1dvU7YMZ27{SBV zt#FF<pxMnNgN~;=GK_DSN^glOVg1iy8tJm8{O5sXH+-U81m+zSY}<P~wd|PgruaFv z``^EPb8b`6T&qyl494Vrd(3A{4)sh=Z85Yq{-d%s=EaFs;%O=M`YdUUd<9%Q66Y$n z2t;ghmG3#{xkVs@q5Z(};BS0)7}yV}ZOFJ;plHB)$C6e5fS$qJ!|D$AAMSql^&`{8 zB}<|`Uzfdfy0mWztEcpVIZHa@?K~$bdmf*3t4woh$;;iVuk1Xm{y?fi**>vf;D(mQ zU#5ADabGgGus<>k{=o2tQQP5iqVI+iVkL7PYA5V$Fg>UgU66nOY#)1elFR1<+84e( z@jb)7hj-1?2i||rZ#r!3*j)NAcOT=?2+3Z?_}mT#1_sp<*NBpo#FA92<f7EXl2isG z10zFS0~1{%qYy(=D`Rsj6H9Fa11kdq@1A=OC>nC}Q!>*kaci);nDz%0VV<sjF6*2U FngCJr$7%oo literal 0 HcmV?d00001 diff --git a/Ring/Ring/Resources/Images.xcassets/ic_save.imageset/Contents.json b/Ring/Ring/Resources/Images.xcassets/ic_save.imageset/Contents.json new file mode 100644 index 000000000..35c0d5881 --- /dev/null +++ b/Ring/Ring/Resources/Images.xcassets/ic_save.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "filename" : "tray-arrow-down.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "tray-arrow-down-2.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "tray-arrow-down-4.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Ring/Ring/Resources/Images.xcassets/ic_save.imageset/tray-arrow-down-2.png b/Ring/Ring/Resources/Images.xcassets/ic_save.imageset/tray-arrow-down-2.png new file mode 100644 index 0000000000000000000000000000000000000000..91ede05ff54b0705ab75972dde0002807dd11942 GIT binary patch literal 760 zcmeAS@N?(olHy`uVBq!ia0y~yU@!w=4mJh`h91|fy9^8rEa{HEjtmSN`?>!lvNA9* zC?tCX`7$t6sWC7#v@kIIVqjosc)`F>YQVtoDuIE)Y6b&?c)^@qfi?^b3~Wi>?k)^q z@Y8vBJp%&+XMsm#F#`ib0vLbDb~?|%!1&A4#W5t~-rHIB-qC>)$L71gdh{fvyLoZ+ zbH1{I55g`Tq3nV|Q<W|qy&JgSX}eOArmOhTgE##bEKJee!QJFC`A5lzWlNs_JGs|H zeYyFb{hE8eKihL&UtRoOweSH0p7|HbnLUjerp^6icrt$B)BmA4afQ!nzsqv#<|V16 zB|SZI<WyVdR9<0iX|-vsOME@^**2V;vc!L({gk{JcQcamJ!bh?%*)=@zhvFw$G`t~ zPr3BuQ-@_xET5C--e4p9>iyIH9#PplEyO?ab*;uuUC;BJjsM=nOp7?>ry2FRSYV~x zPG8TtCOb_wdc0C|%~D?x(tqbT&(5hWRi@5a)?50u!@Vc&J|FqoV^!-nkIB&!uY?s^ zO}bXwe!F<LSj~TS&$UcJxt`gq)jQtoe9lv|MCm8*0j-9zzRT7O*F5WFOZfCRE1i5_ zQR#8(mGV}%PZ!+T@|_J%$}?y&$O-dpX9)3>`}yj9=`Qt3wgf?k&CCC=By>LMJb&+A zezeED$EnX}O$?d(WaayP>ubzAp6IEB?poh|u~;MNu*j>V@(ZcSDMu%*+qf)xLW!hR zhJ4b@$wyvsZf(7A@^`^ov84Tu-csxZ%n4>Cx8|5I>{0!B{fF4@V~d2f81kIe&mGil za1G}=@#A{K5>EzchGz^uEoKE33<azYxKHlBkjQh`fakD*+m!o^FMpi7cst60fq_A_ z#5JNMC9x#cD!C{%u_Tqj$iT=@*T6*A$SB0n)XK!r%GgBPz`)ADfMxgPSQHJp`6-!c WmAEyOKFfXxN`;=TelF{r5}E)%WHVp@ literal 0 HcmV?d00001 diff --git a/Ring/Ring/Resources/Images.xcassets/ic_save.imageset/tray-arrow-down-4.png b/Ring/Ring/Resources/Images.xcassets/ic_save.imageset/tray-arrow-down-4.png new file mode 100644 index 0000000000000000000000000000000000000000..2564dcfee939661f5ab4d6b58bd2473999dc1902 GIT binary patch literal 1092 zcmeAS@N?(olHy`uVBq!ia0y~yU<d?Z4mJh`hE@GuW(*7rEa{HEjtmSN`?>!lvNA9* zC?tCX`7$t6sWC7#v@kIIVqjosc)`F>YQVtoDuIE)Y6b&?c)^@qfi?^b3~Wi>?k)^q z@Y8vBJp%&+XMsm#F#`ib0vLbDb~?|%!0hJf;uunK>+PJA{k(x9?faWsX0QrMAKagy zoOrQJthb{>Kv3`>bA7|o#Poxcrtw*+xNs;sF7MRraCzgiS9sd9m+2RmCRdp5E&o0D z)w!S3-d|a3^}Krdz1=&v2MMq^3a~hCHh9kxrI=}Iu-5g(=9zatoy(6=c)2v{^Cj!; zVLqSy5|jjlgvme`OgkrC^PF{OX}0gAEeoT!ma=+QZ|+^j=ozdfbN`#$RF%mkCf0TP z%ipeKUA5{&nQEGB#hdwt?0aOFh(&m&SPL099xpy7`=xARRKm&=yM(-1{ym$peey1* z<=$1Rihf;vbbeW%i|1t3uc14x1$%DZbN7nq&VWmkxsLw5J~31^jp?*iz@&MeQE%QZ ziJkaa#dd}ErmiDWvOgj&G3ATT?V6+gGER14s!%pZjY{Ue?afn^-iL`ssu&7aEIPDP z&QGGTzGIH^OTX4fzg2mkA8FfMAz7|+_T#$BH7@5Slq_y)@!O%BxvhNdU4hEl9u~fn ziBgk)FIlrh^}w|`AKv=Si<oM{yo2M5%9^KY7qZjjtxjZ{ZqQP%Id!J(^}G$sA4uQ7 zo_SPi=7f0fD~-*Q($r35#ve4T`gCmPTJ>2s-4{%s*!QMo^?@kk>i41DuNSSES<7g~ zD1OW4sAxj-%-T<D4%epMKC_Q$cH?K$_Bb}PSvS+)OWjdDwBm&Bft_8>dKU#6F9>vg z2zQ^SWBEl#nQ4E;^FOnXd9WC4mOsF^!2bCJ;{#^Kzn{&0J>SGjsi*4>=bRtvCJxN3 zPRt-PVjbeYUCd0_J+t=vxwWE8t@Gn%+~oJ%-Qc+3{F%C{=TqF1Rhd#A^;JF(bW_fM z{?X0D#{rHm?k~Q$WLx;-Qu*yOL|He+$h&IaKHd2IUZ`nDYTCC|p$k5^N==GmIK5|c zKEwL6Ihk=A-k5*vDF4R$upn^58sX(en;5rT+Rf$uj>*>5JHK`A4Bi-ap33k?M|2Zv zZ{A9MDRTVGJ;v;Yy9a6=?oI7AJoV}h^BTj=+f$jd_Ak`jeN_D7hojwQKl%1#URqTN zN!l<{xI5lR_vztH7i1zB7#LJbTq8<S5=&C8l8aIkOHvt(42%qQ4NP>6j6w`etxOE9 qOboOQ46F<cei{A?N70a*pOTqYiCaT=j@xTcj`no*b6Mw<&;$Sv@V`O; literal 0 HcmV?d00001 diff --git a/Ring/Ring/Resources/Images.xcassets/ic_save.imageset/tray-arrow-down.png b/Ring/Ring/Resources/Images.xcassets/ic_save.imageset/tray-arrow-down.png new file mode 100644 index 0000000000000000000000000000000000000000..16f6e969a6fcf77feedeeca48a62a6a1fb52d94b GIT binary patch literal 546 zcmeAS@N?(olHy`uVBq!ia0y~yV2}o34mJh`hTbb*LKzqsSkfJR9T^xl_H+M9WMyDr zP)PO&@?~JCQe$9fXklRZ#lXPO@PdJ%)PRBERRRNp)eHs(@q#(K0&N%=7}%1$-CY>K z;HUHMdIkmt&H|6fVg?3=1Tg-P?R1`jfl<ZN#WBR=_}eMAUd(|at>;Cxt))91V;ZtM zgp+jcIB^xSW{J97N>OLrK2a$lH(5bQ$5miTg5u0?n?LZ`wY5C4Y<8V-dC$z#w%@Ct zpY0Dh$y(B=c%k=e-v7pOm6suxvey*8w)bD!l&iqAgVitW%a(??g>xCE|H+ug*w}cT zjWOfh*4IiKqzpJ4-U_hEFvr*Z=aCWd4QF4*Sn%fhV}*+%D~|KLWiWS0c0II5%^|mO z`NPjv-3iL(->2uK>3d9l$$2CG(*5uA0>h$zUp3!c?&<5vA6GTyipl2frRgrU>1p3I zU;12{yJXwU+9N*}|NoU{Hn~Lbb+=zwNaCiM>%$LJa&9g!F0N-)aq!yd)u@sj!B?T7 zIdMy{)25J1duQ&Q@<T-{chf#bD~r5I&a1&uqFUk_QIe8al4_M)l$uzQ%3x$*WT<Ok zqHAOnVrXh*VrXS#scm3jWni%K*d}HM1_p$N-29Zxv`X9>Zp`M|4~j2OS3j3^P6<r_ D@gl>t literal 0 HcmV?d00001 diff --git a/Ring/Ring/Resources/Images.xcassets/ic_share.imageset/Contents.json b/Ring/Ring/Resources/Images.xcassets/ic_share.imageset/Contents.json new file mode 100644 index 000000000..c9e7ccc34 --- /dev/null +++ b/Ring/Ring/Resources/Images.xcassets/ic_share.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "export-variant-3.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "export-variant-4.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "export-variant.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Ring/Ring/Resources/Images.xcassets/ic_share.imageset/export-variant-3.png b/Ring/Ring/Resources/Images.xcassets/ic_share.imageset/export-variant-3.png new file mode 100644 index 0000000000000000000000000000000000000000..16dd4d7800bb414d63df05af1e6dffd5c585273a GIT binary patch literal 386 zcmeAS@N?(olHy`uVBq!ia0y~yV2}V|4mJh`h6m-gKNuJoSkfJR9T^xl_H+M9WMyDr zP)PO&@?~JCQe$9fXklRZ#lXPO@PdJ%)PRBERRRNp)eHs(@q#(K0&N%=7}%1$-CY>K z;HUHMdIkmt&H|6fVg?3=1Tg-P?R1`jfuY6I#WBR=_}eK5c^ec2Sl;)f-k7kF|J8@g zxk>Vm*@c^>91l)7QP(J2)~^sWsX=f?(X4zg?vH0KJL#R-tjehH%xCf!HbsU6hH16@ z<(+eMF0@}s_`7JmmetIYe^xzL&%S?t0aMLZ!`Y#s-QEF=_6&1uFT1+zx>0}sWSfcs z-=`yYp5#4QAZk-^N}g>lPc;JrgKCLuL`h0wNvc(HQEFmIDua=Mk)f`EiLQ}Rh@q*K sv5A$Dp|*j6m4QL1wbEh~4Y~O#nQ4`{HK?S`k_Ux{r>mdKI;Vst04%6^cmMzZ literal 0 HcmV?d00001 diff --git a/Ring/Ring/Resources/Images.xcassets/ic_share.imageset/export-variant-4.png b/Ring/Ring/Resources/Images.xcassets/ic_share.imageset/export-variant-4.png new file mode 100644 index 0000000000000000000000000000000000000000..74cc4fafcf3c29ab6cc58f20e771aae97309c9bf GIT binary patch literal 557 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?F<YIEa{HEjtmSN`?>!lvNA9* zC?tCX`7$t6sWC7#v@kIIVqjosc)`F>YQVtoDuIE)Y6b&?c)^@qfi?^b3~Wi>?k)^q z@Y8vBJp%&+XMsm#F#`ib0vLbDb~?|%z^L!(;uw-~@9hlRyh9EGt@jsf;q<E7zR>sN zM~%xaca*y(S-Do7bMWOBs%~_5j7i`;|0~^gw?TY*=HB0S=c`imAKWoe30JwlwwQ-A zK6sjoa!sUA_@U{F3tP@dD;d?y6OujrT=k;ypA4Z%0X^c}ohp&p)4US2Vh+sGV4Ng+ zQh(CJTeokNKAa|QJ>ieyPKl!*?El1Nt4AH+V=`r*bX7t$g1JKSX=ce|$ph_X9=W+H z%nk0Pi`-^kxs~x`8@G~?yw-))^ZiTj*On}tQ^=Sdb3jUb!?KFT_7LIaznHUwYFNP{ z$5_|EB-Nf}n=n+Y(*HH5neV|0-j0<A?zc3~{*%R=<o7I&F{i+8^_)qz4d#M3xAF#Q zD=0-Xcr)C$$X|Yig<-zo!?`jRjE5Co_pP6PVlD#%gKCLuL`h0wNvc(HQEFmIDua=M zk)f`EiLQ}Rh@q*Kv5A$DnYMv}m4U&8IWoE^8glbfGSez?Yj}8Y$wW}pdAj<!taD0e F0syf$!G8b% literal 0 HcmV?d00001 diff --git a/Ring/Ring/Resources/Images.xcassets/ic_share.imageset/export-variant.png b/Ring/Ring/Resources/Images.xcassets/ic_share.imageset/export-variant.png new file mode 100644 index 0000000000000000000000000000000000000000..44ab9a3edc40397e1ac9a818bdb47bb6b1157a99 GIT binary patch literal 713 zcmeAS@N?(olHy`uVBq!ia0y~yVDJE84mJh`hS0a0-5D4dSkfJR9T^xl_H+M9WMyDr zP)PO&@?~JCQe$9fXklRZ#lXPO@PdJ%)PRBERRRNp)eHs(@q#(K0&N%=7}%1$-CY>K z;HUHMdIkmt&H|6fVg?3=1Tg-P?R1`jf$_Abi(^Q|t+%%g{SG^bv^<P#@nBXzAv$3} zvvUD+)`BMEfFiak3-1*jEe{kPu+3W7F;Qts#-8(K*E*lSpT6hshNDbo59UaFtGxHL zn=&tX!c~iU$;_Sg7H&~_ynPd;9`s1BR6J)f(WvlPtH=>a_8{*$cimobq?=E)I(Acc z$-DxywkclB*E(V>W4w+&zpZV1;DYgl6AntuojfXB%oFEK*rRNz9BKM<=AEk-c>5Vb zZEx$IJn0td>F0T_m|u6NbROrHJK85RQ`X;n-t%ksN2UiVzYkxVlU;P)@!;x0z6Ul- zzs%jNQPh3crGV{%X2a?`u7V3rU$V{({Pt#I^c(I2i7C4dZI@sO(OkNCU!&Fky_qNC zFF#k9*yzG0B<YcYo$-4qJ44$?{b!{=xo|7O)IZVwR(Qlmf%NJAm%iUff7f(E^h;dT zblqPkVg(qhwDz7BOFb`FE1KALbn@%lS7Z)Edc9On5Bw$?vj25KVEuz_ChP?hyP3CC ztepOYu_0SW?!CR+hnK(QRx|8jtYG^faj@{**L(UUvXfu_TV}7#z`&qd;u=wsl30>z zm0XmXSdz+MWME{dYha>lWE5g(YGrI}Wo)KxU|?lnu<sT7DHIL4`6-!cmAEyW-@d;L Pl&m~m{an^LB{Ts58vqYU literal 0 HcmV?d00001 diff --git a/Ring/Ring/Resources/en.lproj/Localizable.strings b/Ring/Ring/Resources/en.lproj/Localizable.strings index c9d80a5e2..6eaee9da8 100644 --- a/Ring/Ring/Resources/en.lproj/Localizable.strings +++ b/Ring/Ring/Resources/en.lproj/Localizable.strings @@ -26,6 +26,10 @@ "global.versionName" = "Together"; "global.close" = "Close"; "global.share" = "Share"; +"global.forward" = "Forward"; +"global.save" = "Save"; +"global.resend" = "Resend"; +"global.preview" = "Preview"; // Scan "scan.badQrCode" = "Bad QR code"; @@ -50,6 +54,7 @@ "conversation.messagePlaceholder" = "Write message to "; "conversation.explanationSendingLocationTo" = "You are currently sharing your location with "; "conversation.explanationReceivingLocationFrom" = "You are currently receiving a live location from "; +"conversation.errorSavingImage" = "Failed to save image to galery"; //Invitations "invitations.noInvitations" = "No invitations"; -- GitLab