commit
27f273e823
|
@ -7173,7 +7173,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CURRENT_PROJECT_VERSION = 387;
|
||||
CURRENT_PROJECT_VERSION = 388;
|
||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
|
@ -7212,7 +7212,7 @@
|
|||
"$(SRCROOT)",
|
||||
);
|
||||
LLVM_LTO = NO;
|
||||
MARKETING_VERSION = 2.2.2;
|
||||
MARKETING_VERSION = 2.2.3;
|
||||
OTHER_LDFLAGS = "$(inherited)";
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
|
||||
|
@ -7245,7 +7245,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CURRENT_PROJECT_VERSION = 387;
|
||||
CURRENT_PROJECT_VERSION = 388;
|
||||
DEVELOPMENT_TEAM = SUQ8J2PCT7;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
|
@ -7284,7 +7284,7 @@
|
|||
"$(SRCROOT)",
|
||||
);
|
||||
LLVM_LTO = NO;
|
||||
MARKETING_VERSION = 2.2.2;
|
||||
MARKETING_VERSION = 2.2.3;
|
||||
OTHER_LDFLAGS = "$(inherited)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
|
||||
PRODUCT_NAME = Session;
|
||||
|
|
|
@ -28,7 +28,8 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
|
|||
result.clipsToBounds = true
|
||||
result.themeBackgroundColor = .backgroundSecondary
|
||||
result.isHidden = !call.isVideoEnabled
|
||||
result.layer.cornerRadius = 10
|
||||
result.layer.cornerRadius = UIDevice.current.isIPad ? 20 : 10
|
||||
result.layer.masksToBounds = true
|
||||
result.set(.width, to: LocalVideoView.width)
|
||||
result.set(.height, to: LocalVideoView.height)
|
||||
result.makeViewDraggable()
|
||||
|
@ -463,6 +464,8 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
|
|||
}
|
||||
|
||||
@objc func didChangeDeviceOrientation(notification: Notification) {
|
||||
if UIDevice.current.isIPad { return }
|
||||
|
||||
func rotateAllButtons(rotationAngle: CGFloat) {
|
||||
let transform = CGAffineTransform(rotationAngle: rotationAngle)
|
||||
|
||||
|
|
|
@ -16,6 +16,16 @@ class RemoteVideoView: TargetView {
|
|||
override func renderFrame(_ frame: RTCVideoFrame?) {
|
||||
super.renderFrame(frame)
|
||||
guard let frame = frame else { return }
|
||||
if UIDevice.current.isIPad {
|
||||
DispatchMainThreadSafe {
|
||||
#if targetEnvironment(simulator)
|
||||
self.contentMode = .scaleAspectFit
|
||||
#else
|
||||
self.videoContentMode = .scaleAspectFit
|
||||
#endif
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
DispatchMainThreadSafe {
|
||||
let frameRatio = Double(frame.height) / Double(frame.width)
|
||||
|
@ -78,8 +88,8 @@ class RemoteVideoView: TargetView {
|
|||
|
||||
class LocalVideoView: TargetView {
|
||||
|
||||
static let width: CGFloat = 80
|
||||
static let height: CGFloat = 173
|
||||
static let width: CGFloat = UIDevice.current.isIPad ? 160 : 80
|
||||
static let height: CGFloat = UIDevice.current.isIPad ? 346: 173
|
||||
|
||||
override func renderFrame(_ frame: RTCVideoFrame?) {
|
||||
super.renderFrame(frame)
|
||||
|
|
|
@ -8,7 +8,8 @@ final class MiniCallView: UIView, RTCVideoViewDelegate {
|
|||
var callVC: CallVC
|
||||
|
||||
// MARK: UI
|
||||
private static let defaultSize: CGFloat = 100
|
||||
private static let defaultSize: CGFloat = UIDevice.current.isIPad ? 200 : 100
|
||||
private static let defaultVideoSize: CGFloat = UIDevice.current.isIPad ? 320 : 160
|
||||
private let topMargin = (UIApplication.shared.keyWindow?.safeAreaInsets.top ?? 0) + Values.veryLargeSpacing
|
||||
private let bottomMargin = (UIApplication.shared.keyWindow?.safeAreaInsets.bottom ?? 0)
|
||||
|
||||
|
@ -102,7 +103,7 @@ final class MiniCallView: UIView, RTCVideoViewDelegate {
|
|||
|
||||
private func setUpViewHierarchy() {
|
||||
self.clipsToBounds = true
|
||||
self.layer.cornerRadius = 10
|
||||
self.layer.cornerRadius = UIDevice.current.isIPad ? 20 : 10
|
||||
self.width = self.set(.width, to: MiniCallView.defaultSize)
|
||||
self.height = self.set(.height, to: MiniCallView.defaultSize)
|
||||
|
||||
|
@ -190,8 +191,8 @@ final class MiniCallView: UIView, RTCVideoViewDelegate {
|
|||
|
||||
func videoView(_ videoView: RTCVideoRenderer, didChangeVideoSize size: CGSize) {
|
||||
let newSize = CGSize(
|
||||
width: min(160.0, 160.0 * size.width / size.height),
|
||||
height: min(160.0, 160.0 * size.height / size.width)
|
||||
width: min(Self.defaultVideoSize, Self.defaultVideoSize * size.width / size.height),
|
||||
height: min(Self.defaultVideoSize, Self.defaultVideoSize * size.height / size.width)
|
||||
)
|
||||
persistCurrentPosition(newSize: newSize)
|
||||
self.width?.constant = newSize.width
|
||||
|
|
|
@ -32,7 +32,7 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate
|
|||
]
|
||||
private var selectedContacts: Set<String> = []
|
||||
private var searchText: String = ""
|
||||
|
||||
|
||||
// MARK: - Components
|
||||
|
||||
private static let textFieldHeight: CGFloat = 50
|
||||
|
|
|
@ -284,6 +284,11 @@ final class ContextMenuVC: UIViewController {
|
|||
}
|
||||
}
|
||||
|
||||
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
super.viewWillTransition(to: size, with: coordinator)
|
||||
snDismiss()
|
||||
}
|
||||
|
||||
func calculateFrame(menuHeight: CGFloat, spacing: CGFloat) -> CGRect {
|
||||
var finalFrame: CGRect = frame
|
||||
let ratio: CGFloat = (frame.width / frame.height)
|
||||
|
|
|
@ -1578,6 +1578,11 @@ extension ConversationVC:
|
|||
case .typingIndicator, .dateHeader: break
|
||||
|
||||
case .textOnlyMessage:
|
||||
if cellViewModel.body == nil, let linkPreview: LinkPreview = cellViewModel.linkPreview {
|
||||
UIPasteboard.general.string = linkPreview.url
|
||||
return
|
||||
}
|
||||
|
||||
UIPasteboard.general.string = cellViewModel.body
|
||||
|
||||
case .audio, .genericAttachment, .mediaMessage:
|
||||
|
|
|
@ -40,6 +40,8 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
|
|||
var audioRecorder: AVAudioRecorder?
|
||||
var audioTimer: Timer?
|
||||
|
||||
private var searchBarWidth: NSLayoutConstraint?
|
||||
|
||||
// Context menu
|
||||
var contextMenuWindow: ContextMenuWindow?
|
||||
var contextMenuVC: ContextMenuVC?
|
||||
|
@ -490,6 +492,12 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
|
|||
stopObservingChanges()
|
||||
}
|
||||
|
||||
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
super.viewWillTransition(to: size, with: coordinator)
|
||||
searchBarWidth?.constant = size.width - 32
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
// MARK: - Updating
|
||||
|
||||
private func startObservingChanges(didReturnFromBackground: Bool = false) {
|
||||
|
@ -1513,7 +1521,7 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
|
|||
searchBar.sizeToFit()
|
||||
searchBar.layoutMargins = UIEdgeInsets.zero
|
||||
searchBarContainer.set(.height, to: 44)
|
||||
searchBarContainer.set(.width, to: UIScreen.main.bounds.width - 32)
|
||||
searchBarWidth = searchBarContainer.set(.width, to: UIScreen.main.bounds.width - 32)
|
||||
searchBarContainer.addSubview(searchBar)
|
||||
navigationItem.titleView = searchBarContainer
|
||||
|
||||
|
|
|
@ -13,7 +13,6 @@ final class ReactionContainerView: UIView {
|
|||
private var collapsedCount: Int = 0
|
||||
private var showingAllReactions: Bool = false
|
||||
private var showNumbers: Bool = true
|
||||
private var maxEmojisPerLine = isIPhone6OrSmaller ? 5 : 6
|
||||
private var oldSize: CGSize = .zero
|
||||
|
||||
var reactions: [ReactionViewModel] = []
|
||||
|
@ -65,7 +64,7 @@ final class ReactionContainerView: UIView {
|
|||
textLabel.setContentCompressionResistancePriority(.required, for: .vertical)
|
||||
textLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
|
||||
textLabel.font = .systemFont(ofSize: Values.verySmallFontSize)
|
||||
textLabel.text = "Show less"
|
||||
textLabel.text = "EMOJI_REACTS_SHOW_LESS".localized()
|
||||
textLabel.themeTextColor = .textPrimary
|
||||
|
||||
let result: UIView = UIView()
|
||||
|
|
|
@ -168,7 +168,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
var result = groupThreadHSpacing + profilePictureSize + groupThreadHSpacing
|
||||
|
||||
if UIDevice.current.isIPad {
|
||||
result += CGFloat(UIScreen.main.bounds.width / 2 - 88)
|
||||
result += 168
|
||||
}
|
||||
|
||||
return result
|
||||
|
@ -503,7 +503,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
let quoteView: QuoteView = QuoteView(
|
||||
for: .regular,
|
||||
authorId: quote.authorId,
|
||||
quotedText: quote.body,
|
||||
quotedText: quote.body ?? "QUOTED_MESSAGE_NOT_FOUND".localized(),
|
||||
threadVariant: cellViewModel.threadVariant,
|
||||
currentUserPublicKey: cellViewModel.currentUserPublicKey,
|
||||
currentUserBlindedPublicKey: cellViewModel.currentUserBlindedPublicKey,
|
||||
|
@ -1025,11 +1025,12 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
|
||||
static func getMaxWidth(for cellViewModel: MessageViewModel, includingOppositeGutter: Bool = true) -> CGFloat {
|
||||
let screen: CGRect = UIScreen.main.bounds
|
||||
let width: CGFloat = UIDevice.current.isIPad ? screen.width * 0.75 : screen.width
|
||||
let oppositeEdgePadding: CGFloat = (includingOppositeGutter ? gutterSize : contactThreadHSpacing)
|
||||
|
||||
switch cellViewModel.variant {
|
||||
case .standardOutgoing:
|
||||
return (screen.width - contactThreadHSpacing - oppositeEdgePadding)
|
||||
return (width - contactThreadHSpacing - oppositeEdgePadding)
|
||||
|
||||
case .standardIncoming, .standardIncomingDeleted:
|
||||
let isGroupThread = (
|
||||
|
@ -1038,7 +1039,7 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
)
|
||||
let leftGutterSize = (isGroupThread ? leftGutterSize : contactThreadHSpacing)
|
||||
|
||||
return (screen.width - leftGutterSize - oppositeEdgePadding)
|
||||
return (width - leftGutterSize - oppositeEdgePadding)
|
||||
|
||||
default: preconditionFailure()
|
||||
}
|
||||
|
|
|
@ -241,7 +241,24 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
|||
),
|
||||
accessibilityIdentifier: "\(ThreadSettingsViewModel.self).copy_thread_id",
|
||||
onTap: {
|
||||
UIPasteboard.general.string = threadId
|
||||
switch threadVariant {
|
||||
case .contact, .closedGroup:
|
||||
UIPasteboard.general.string = threadId
|
||||
|
||||
case .openGroup:
|
||||
guard
|
||||
let server: String = threadViewModel.openGroupServer,
|
||||
let roomToken: String = threadViewModel.openGroupRoomToken,
|
||||
let publicKey: String = threadViewModel.openGroupPublicKey
|
||||
else { return }
|
||||
|
||||
UIPasteboard.general.string = OpenGroup.urlFor(
|
||||
server: server,
|
||||
roomToken: roomToken,
|
||||
publicKey: publicKey
|
||||
)
|
||||
}
|
||||
|
||||
self?.showToast(
|
||||
text: "copied".localized(),
|
||||
backgroundColor: .backgroundSecondary
|
||||
|
@ -297,7 +314,10 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
|||
with: "vc_conversation_settings_invite_button_title".localized(),
|
||||
excluding: Set()
|
||||
) { [weak self] selectedUsers in
|
||||
self?.addUsersToOpenGoup(selectedUsers: selectedUsers)
|
||||
self?.addUsersToOpenGoup(
|
||||
threadViewModel: threadViewModel,
|
||||
selectedUsers: selectedUsers
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -561,13 +581,20 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
|||
self.transitionToScreen(navController, transitionType: .present)
|
||||
}
|
||||
|
||||
private func addUsersToOpenGoup(selectedUsers: Set<String>) {
|
||||
let threadId: String = self.threadId
|
||||
private func addUsersToOpenGoup(threadViewModel: SessionThreadViewModel, selectedUsers: Set<String>) {
|
||||
guard
|
||||
let name: String = threadViewModel.openGroupName,
|
||||
let server: String = threadViewModel.openGroupServer,
|
||||
let roomToken: String = threadViewModel.openGroupRoomToken,
|
||||
let publicKey: String = threadViewModel.openGroupPublicKey
|
||||
else { return }
|
||||
|
||||
dependencies.storage.writeAsync { db in
|
||||
guard let openGroup: OpenGroup = try OpenGroup.fetchOne(db, id: threadId) else { return }
|
||||
|
||||
let urlString: String = "\(openGroup.server)/\(openGroup.roomToken)?public_key=\(openGroup.publicKey)"
|
||||
let urlString: String = OpenGroup.urlFor(
|
||||
server: server,
|
||||
roomToken: roomToken,
|
||||
publicKey: publicKey
|
||||
)
|
||||
|
||||
try selectedUsers.forEach { userId in
|
||||
let thread: SessionThread = try SessionThread.fetchOrCreate(db, id: userId, variant: .contact)
|
||||
|
@ -575,7 +602,7 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
|||
try LinkPreview(
|
||||
url: urlString,
|
||||
variant: .openGroupInvitation,
|
||||
title: openGroup.name
|
||||
title: name
|
||||
)
|
||||
.save(db)
|
||||
|
||||
|
|
|
@ -57,6 +57,8 @@ class GlobalSearchViewController: BaseVC, UITableViewDelegate, UITableViewDataSo
|
|||
|
||||
return result
|
||||
}()
|
||||
|
||||
private var searchBarWidth: NSLayoutConstraint?
|
||||
|
||||
internal lazy var tableView: UITableView = {
|
||||
let result: UITableView = UITableView(frame: .zero, style: .grouped)
|
||||
|
@ -98,6 +100,11 @@ class GlobalSearchViewController: BaseVC, UITableViewDelegate, UITableViewDataSo
|
|||
super.viewWillDisappear(animated)
|
||||
searchBar.resignFirstResponder()
|
||||
}
|
||||
|
||||
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
super.viewWillTransition(to: size, with: coordinator)
|
||||
searchBarWidth?.constant = size.width - 32
|
||||
}
|
||||
|
||||
private func setupNavigationBar() {
|
||||
// This is a workaround for a UI issue that the navigation bar can be a bit higher if
|
||||
|
@ -108,7 +115,7 @@ class GlobalSearchViewController: BaseVC, UITableViewDelegate, UITableViewDataSo
|
|||
searchBar.sizeToFit()
|
||||
searchBar.layoutMargins = UIEdgeInsets.zero
|
||||
searchBarContainer.set(.height, to: 44)
|
||||
searchBarContainer.set(.width, to: UIScreen.main.bounds.width - 32)
|
||||
searchBarWidth = searchBarContainer.set(.width, to: UIScreen.main.bounds.width - 32)
|
||||
searchBarContainer.addSubview(searchBar)
|
||||
navigationItem.titleView = searchBarContainer
|
||||
|
||||
|
|
|
@ -235,8 +235,8 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat
|
|||
loadingConversationsLabel.isHidden = true
|
||||
|
||||
// Show the empty state if there is no data
|
||||
clearAllButton.isHidden = updatedData.isEmpty
|
||||
emptyStateLabel.isHidden = !updatedData.isEmpty
|
||||
clearAllButton.isHidden = !(updatedData.first?.elements.isEmpty == false)
|
||||
emptyStateLabel.isHidden = !clearAllButton.isHidden
|
||||
|
||||
CATransaction.begin()
|
||||
CATransaction.setCompletionBlock { [weak self] in
|
||||
|
|
|
@ -92,32 +92,43 @@ final class NewDMVC: BaseVC, UIPageViewControllerDataSource, UIPageViewControlle
|
|||
navigationItem.leftBarButtonItem = closeButton
|
||||
}
|
||||
|
||||
// Set up tab bar
|
||||
view.addSubview(tabBar)
|
||||
tabBar.pin(.top, to: .top, of: view)
|
||||
tabBar.pin(.leading, to: .leading, of: view)
|
||||
tabBar.pin(.trailing, to: .trailing, of: view)
|
||||
|
||||
// Set up page VC
|
||||
let containerView: UIView = UIView()
|
||||
view.addSubview(containerView)
|
||||
containerView.pin(.top, to: .bottom, of: tabBar)
|
||||
containerView.pin(.leading, to: .leading, of: view)
|
||||
containerView.pin(.trailing, to: .trailing, of: view)
|
||||
containerView.pin(.bottom, to: .bottom, of: view)
|
||||
|
||||
// Page VC
|
||||
let hasCameraAccess = (AVCaptureDevice.authorizationStatus(for: .video) == .authorized)
|
||||
pages = [ enterPublicKeyVC, (hasCameraAccess ? scanQRCodeWrapperVC : scanQRCodePlaceholderVC) ]
|
||||
pageVC.dataSource = self
|
||||
pageVC.delegate = self
|
||||
pageVC.setViewControllers([ enterPublicKeyVC ], direction: .forward, animated: false, completion: nil)
|
||||
addChild(pageVC)
|
||||
containerView.addSubview(pageVC.view)
|
||||
|
||||
pageVC.view.pin(to: containerView)
|
||||
pageVC.didMove(toParent: self)
|
||||
// Tab bar
|
||||
view.addSubview(tabBar)
|
||||
tabBar.pin(.top, to: .top, of: view)
|
||||
tabBar.pin(.leading, to: .leading, of: view)
|
||||
tabBar.pin(.trailing, to: .trailing, of: view)
|
||||
|
||||
// Page VC constraints
|
||||
let pageVCView = pageVC.view!
|
||||
view.addSubview(pageVCView)
|
||||
pageVCView.pin(.leading, to: .leading, of: view)
|
||||
pageVCView.pin(.top, to: .bottom, of: tabBar)
|
||||
pageVCView.pin(.trailing, to: .trailing, of: view)
|
||||
pageVCView.pin(.bottom, to: .bottom, of: view)
|
||||
|
||||
let navBarHeight: CGFloat = (navigationController?.navigationBar.frame.size.height ?? 0)
|
||||
let statusBarHeight: CGFloat = UIApplication.shared.statusBarFrame.size.height
|
||||
let height: CGFloat = ((navigationController?.view.bounds.height ?? 0) - navBarHeight - TabBar.snHeight - statusBarHeight)
|
||||
let size: CGSize = CGSize(width: UIScreen.main.bounds.width, height: height)
|
||||
enterPublicKeyVC.constrainSize(to: size)
|
||||
scanQRCodePlaceholderVC.constrainSize(to: size)
|
||||
}
|
||||
|
||||
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
super.viewWillTransition(to: size, with: coordinator)
|
||||
let height: CGFloat = (size.height - TabBar.snHeight)
|
||||
let size: CGSize = CGSize(width: size.width, height: height)
|
||||
enterPublicKeyVC.constrainSize(to: size)
|
||||
scanQRCodePlaceholderVC.constrainSize(to: size)
|
||||
}
|
||||
|
||||
// MARK: - General
|
||||
|
||||
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
|
||||
|
@ -351,11 +362,6 @@ private final class EnterPublicKeyVC: UIViewController {
|
|||
result.spacing = UIDevice.current.isIPad ? Values.iPadButtonSpacing : Values.mediumSpacing
|
||||
result.distribution = .fillEqually
|
||||
|
||||
if (UIDevice.current.isIPad) {
|
||||
result.layoutMargins = UIEdgeInsets(top: 0, left: Values.iPadButtonContainerMargin, bottom: 0, right: Values.iPadButtonContainerMargin)
|
||||
result.isLayoutMarginsRelativeArrangement = true
|
||||
}
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
|
@ -396,6 +402,9 @@ private final class EnterPublicKeyVC: UIViewController {
|
|||
return result
|
||||
}()
|
||||
|
||||
private var viewWidth: NSLayoutConstraint?
|
||||
private var viewHeight: NSLayoutConstraint?
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
override func viewDidLoad() {
|
||||
|
@ -428,12 +437,10 @@ private final class EnterPublicKeyVC: UIViewController {
|
|||
)
|
||||
mainStackView.isLayoutMarginsRelativeArrangement = true
|
||||
view.addSubview(mainStackView)
|
||||
|
||||
mainStackView.pin(.top, to: .top, of: view)
|
||||
mainStackView.pin(.leading, to: .leading, of: view)
|
||||
mainStackView.pin(.trailing, to: .trailing, of: view)
|
||||
bottomConstraint = mainStackView.pin(.bottom, to: .bottom, of: view, withInset: -bottomMargin)
|
||||
|
||||
|
||||
mainStackView.pin([ UIView.HorizontalEdge.leading, UIView.HorizontalEdge.trailing, UIView.VerticalEdge.top ], to: view)
|
||||
bottomConstraint = mainStackView.pin(.bottom, to: .bottom, of: view, withInset: bottomMargin)
|
||||
|
||||
// Dismiss keyboard on tap
|
||||
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
|
||||
view.addGestureRecognizer(tapGestureRecognizer)
|
||||
|
@ -450,6 +457,27 @@ private final class EnterPublicKeyVC: UIViewController {
|
|||
|
||||
// MARK: - General
|
||||
|
||||
func constrainSize(to size: CGSize) {
|
||||
if viewWidth == nil {
|
||||
viewWidth = view.set(.width, to: size.width)
|
||||
} else {
|
||||
viewWidth?.constant = size.width
|
||||
}
|
||||
|
||||
if viewHeight == nil {
|
||||
viewHeight = view.set(.height, to: size.height)
|
||||
} else {
|
||||
viewHeight?.constant = size.height
|
||||
}
|
||||
|
||||
if (UIDevice.current.isIPad) {
|
||||
let iPadButtonContainerMargin: CGFloat = (size.width - Values.iPadButtonSpacing) / 2 - Values.iPadButtonWidth - Values.largeSpacing
|
||||
buttonContainer.layoutMargins = UIEdgeInsets(top: 0, left: iPadButtonContainerMargin, bottom: 0, right: iPadButtonContainerMargin)
|
||||
buttonContainer.isLayoutMarginsRelativeArrangement = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func setSessionId(to sessionId: String) {
|
||||
publicKeyTextView.insertText(sessionId)
|
||||
}
|
||||
|
@ -609,6 +637,9 @@ private final class EnterPublicKeyVC: UIViewController {
|
|||
private final class ScanQRCodePlaceholderVC: UIViewController {
|
||||
weak var newDMVC: NewDMVC!
|
||||
|
||||
private var viewWidth: NSLayoutConstraint?
|
||||
private var viewHeight: NSLayoutConstraint?
|
||||
|
||||
override func viewDidLoad() {
|
||||
// Remove background color
|
||||
view.themeBackgroundColor = .clear
|
||||
|
@ -638,10 +669,27 @@ private final class ScanQRCodePlaceholderVC: UIViewController {
|
|||
// Set up constraints
|
||||
view.addSubview(stackView)
|
||||
stackView.pin(.leading, to: .leading, of: view, withInset: Values.massiveSpacing)
|
||||
stackView.pin(.trailing, to: .trailing, of: view, withInset: -Values.massiveSpacing)
|
||||
stackView.center(.vertical, in: view, withInset: -16) // Makes things appear centered visually
|
||||
view.pin(.trailing, to: .trailing, of: stackView, withInset: Values.massiveSpacing)
|
||||
|
||||
let verticalCenteringConstraint = stackView.center(.vertical, in: view)
|
||||
verticalCenteringConstraint.constant = -16 // Makes things appear centered visually
|
||||
}
|
||||
|
||||
func constrainSize(to size: CGSize) {
|
||||
if viewWidth == nil {
|
||||
viewWidth = view.set(.width, to: size.width)
|
||||
} else {
|
||||
viewWidth?.constant = size.width
|
||||
}
|
||||
|
||||
if viewHeight == nil {
|
||||
viewHeight = view.set(.height, to: size.height)
|
||||
} else {
|
||||
viewHeight?.constant = size.height
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc private func requestCameraAccess() {
|
||||
Permissions.requestCameraPermissionIfNeeded { [weak self] in
|
||||
self?.newDMVC.handleCameraAccessGranted()
|
||||
|
|
|
@ -191,6 +191,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||
// MARK: - Orientation
|
||||
|
||||
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
|
||||
if UIDevice.current.isIPad {
|
||||
return .allButUpsideDown
|
||||
}
|
||||
|
||||
return .portrait
|
||||
}
|
||||
|
||||
|
|
|
@ -144,6 +144,12 @@
|
|||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true/>
|
||||
</dict>
|
||||
|
|
|
@ -450,6 +450,7 @@
|
|||
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
|
||||
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
|
||||
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
|
||||
"QUOTED_MESSAGE_NOT_FOUND" = "Original message not found.";
|
||||
"MEDIA_TAB_TITLE" = "Media";
|
||||
"DOCUMENT_TAB_TITLE" = "Documents";
|
||||
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
|
||||
|
@ -476,6 +477,7 @@
|
|||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"EMOJI_REACTS_SHOW_LESS" = "Show less";
|
||||
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
|
||||
/* New conversation screen*/
|
||||
"vc_new_conversation_title" = "New Conversation";
|
||||
|
|
|
@ -450,6 +450,7 @@
|
|||
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
|
||||
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
|
||||
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
|
||||
"QUOTED_MESSAGE_NOT_FOUND" = "Original message not found.";
|
||||
"MEDIA_TAB_TITLE" = "Media";
|
||||
"DOCUMENT_TAB_TITLE" = "Documents";
|
||||
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
|
||||
|
@ -476,6 +477,7 @@
|
|||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"EMOJI_REACTS_SHOW_LESS" = "Show less";
|
||||
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
|
||||
/* New conversation screen*/
|
||||
"vc_new_conversation_title" = "New Conversation";
|
||||
|
|
|
@ -450,6 +450,7 @@
|
|||
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
|
||||
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
|
||||
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
|
||||
"QUOTED_MESSAGE_NOT_FOUND" = "Original message not found.";
|
||||
"MEDIA_TAB_TITLE" = "Media";
|
||||
"DOCUMENT_TAB_TITLE" = "Documents";
|
||||
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
|
||||
|
@ -476,6 +477,7 @@
|
|||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"EMOJI_REACTS_SHOW_LESS" = "Show less";
|
||||
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
|
||||
/* New conversation screen*/
|
||||
"vc_new_conversation_title" = "New Conversation";
|
||||
|
|
|
@ -450,6 +450,7 @@
|
|||
"SEND_FAILED_NOTIFICATION_BODY" = "پیام شما ارسال نشد.";
|
||||
"INVALID_SESSION_ID_MESSAGE" = "لطفاً شناسه Session را مجدد بررسی کنید.";
|
||||
"INVALID_RECOVERY_PHRASE_MESSAGE" = "لطفاً شناسه بازیابی را مجدد بررسی کنید.";
|
||||
"QUOTED_MESSAGE_NOT_FOUND" = "Original message not found.";
|
||||
"MEDIA_TAB_TITLE" = "مدیا";
|
||||
"DOCUMENT_TAB_TITLE" = "مدارک";
|
||||
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "شما در این مکالمه هیچ مدرکی ندارید.";
|
||||
|
@ -476,6 +477,7 @@
|
|||
"EMOJI_REACTS_NOTIFICATION" = "%@ به پیامی با %@ واکنش نشان میدهد. ";
|
||||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "و ۱ نفر دیگر به این پیام واکنش %@ نشان داده است.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "و %@ نفر دیگر به این پیام واکنش %@ نشان دادهاند.";
|
||||
"EMOJI_REACTS_SHOW_LESS" = "Show less";
|
||||
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
|
||||
/* New conversation screen*/
|
||||
"vc_new_conversation_title" = "مکالمه جدید";
|
||||
|
|
|
@ -450,6 +450,7 @@
|
|||
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
|
||||
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
|
||||
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
|
||||
"QUOTED_MESSAGE_NOT_FOUND" = "Original message not found.";
|
||||
"MEDIA_TAB_TITLE" = "Media";
|
||||
"DOCUMENT_TAB_TITLE" = "Documents";
|
||||
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
|
||||
|
@ -476,6 +477,7 @@
|
|||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"EMOJI_REACTS_SHOW_LESS" = "Show less";
|
||||
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
|
||||
/* New conversation screen*/
|
||||
"vc_new_conversation_title" = "New Conversation";
|
||||
|
|
|
@ -450,6 +450,7 @@
|
|||
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
|
||||
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
|
||||
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
|
||||
"QUOTED_MESSAGE_NOT_FOUND" = "Original message not found.";
|
||||
"MEDIA_TAB_TITLE" = "Media";
|
||||
"DOCUMENT_TAB_TITLE" = "Documents";
|
||||
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
|
||||
|
@ -476,6 +477,7 @@
|
|||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"EMOJI_REACTS_SHOW_LESS" = "Show less";
|
||||
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
|
||||
/* New conversation screen*/
|
||||
"vc_new_conversation_title" = "New Conversation";
|
||||
|
|
|
@ -450,6 +450,7 @@
|
|||
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
|
||||
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
|
||||
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
|
||||
"QUOTED_MESSAGE_NOT_FOUND" = "Original message not found.";
|
||||
"MEDIA_TAB_TITLE" = "Media";
|
||||
"DOCUMENT_TAB_TITLE" = "Documents";
|
||||
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
|
||||
|
@ -476,6 +477,7 @@
|
|||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"EMOJI_REACTS_SHOW_LESS" = "Show less";
|
||||
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
|
||||
/* New conversation screen*/
|
||||
"vc_new_conversation_title" = "New Conversation";
|
||||
|
|
|
@ -450,6 +450,7 @@
|
|||
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
|
||||
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
|
||||
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
|
||||
"QUOTED_MESSAGE_NOT_FOUND" = "Original message not found.";
|
||||
"MEDIA_TAB_TITLE" = "Media";
|
||||
"DOCUMENT_TAB_TITLE" = "Documents";
|
||||
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
|
||||
|
@ -476,6 +477,7 @@
|
|||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"EMOJI_REACTS_SHOW_LESS" = "Show less";
|
||||
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
|
||||
/* New conversation screen*/
|
||||
"vc_new_conversation_title" = "New Conversation";
|
||||
|
|
|
@ -450,6 +450,7 @@
|
|||
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
|
||||
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
|
||||
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
|
||||
"QUOTED_MESSAGE_NOT_FOUND" = "Original message not found.";
|
||||
"MEDIA_TAB_TITLE" = "Media";
|
||||
"DOCUMENT_TAB_TITLE" = "Documents";
|
||||
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
|
||||
|
@ -476,6 +477,7 @@
|
|||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"EMOJI_REACTS_SHOW_LESS" = "Show less";
|
||||
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
|
||||
/* New conversation screen*/
|
||||
"vc_new_conversation_title" = "New Conversation";
|
||||
|
|
|
@ -450,6 +450,7 @@
|
|||
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
|
||||
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
|
||||
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
|
||||
"QUOTED_MESSAGE_NOT_FOUND" = "Original message not found.";
|
||||
"MEDIA_TAB_TITLE" = "Media";
|
||||
"DOCUMENT_TAB_TITLE" = "Documents";
|
||||
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
|
||||
|
@ -476,6 +477,7 @@
|
|||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"EMOJI_REACTS_SHOW_LESS" = "Show less";
|
||||
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
|
||||
/* New conversation screen*/
|
||||
"vc_new_conversation_title" = "New Conversation";
|
||||
|
|
|
@ -450,6 +450,7 @@
|
|||
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
|
||||
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
|
||||
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
|
||||
"QUOTED_MESSAGE_NOT_FOUND" = "Original message not found.";
|
||||
"MEDIA_TAB_TITLE" = "Media";
|
||||
"DOCUMENT_TAB_TITLE" = "Documents";
|
||||
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
|
||||
|
@ -476,6 +477,7 @@
|
|||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"EMOJI_REACTS_SHOW_LESS" = "Show less";
|
||||
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
|
||||
/* New conversation screen*/
|
||||
"vc_new_conversation_title" = "New Conversation";
|
||||
|
|
|
@ -450,6 +450,7 @@
|
|||
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
|
||||
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
|
||||
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
|
||||
"QUOTED_MESSAGE_NOT_FOUND" = "Original message not found.";
|
||||
"MEDIA_TAB_TITLE" = "Media";
|
||||
"DOCUMENT_TAB_TITLE" = "Documents";
|
||||
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
|
||||
|
@ -476,6 +477,7 @@
|
|||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"EMOJI_REACTS_SHOW_LESS" = "Show less";
|
||||
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
|
||||
/* New conversation screen*/
|
||||
"vc_new_conversation_title" = "New Conversation";
|
||||
|
|
|
@ -450,6 +450,7 @@
|
|||
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
|
||||
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
|
||||
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
|
||||
"QUOTED_MESSAGE_NOT_FOUND" = "Original message not found.";
|
||||
"MEDIA_TAB_TITLE" = "Media";
|
||||
"DOCUMENT_TAB_TITLE" = "Documents";
|
||||
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
|
||||
|
@ -476,6 +477,7 @@
|
|||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"EMOJI_REACTS_SHOW_LESS" = "Show less";
|
||||
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
|
||||
/* New conversation screen*/
|
||||
"vc_new_conversation_title" = "New Conversation";
|
||||
|
|
|
@ -450,6 +450,7 @@
|
|||
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
|
||||
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
|
||||
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
|
||||
"QUOTED_MESSAGE_NOT_FOUND" = "Original message not found.";
|
||||
"MEDIA_TAB_TITLE" = "Media";
|
||||
"DOCUMENT_TAB_TITLE" = "Documents";
|
||||
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
|
||||
|
@ -476,6 +477,7 @@
|
|||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"EMOJI_REACTS_SHOW_LESS" = "Show less";
|
||||
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
|
||||
/* New conversation screen*/
|
||||
"vc_new_conversation_title" = "New Conversation";
|
||||
|
|
|
@ -450,6 +450,7 @@
|
|||
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
|
||||
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
|
||||
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
|
||||
"QUOTED_MESSAGE_NOT_FOUND" = "Original message not found.";
|
||||
"MEDIA_TAB_TITLE" = "Media";
|
||||
"DOCUMENT_TAB_TITLE" = "Documents";
|
||||
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
|
||||
|
@ -476,6 +477,7 @@
|
|||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"EMOJI_REACTS_SHOW_LESS" = "Show less";
|
||||
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
|
||||
/* New conversation screen*/
|
||||
"vc_new_conversation_title" = "New Conversation";
|
||||
|
|
|
@ -450,6 +450,7 @@
|
|||
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
|
||||
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
|
||||
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
|
||||
"QUOTED_MESSAGE_NOT_FOUND" = "Original message not found.";
|
||||
"MEDIA_TAB_TITLE" = "Media";
|
||||
"DOCUMENT_TAB_TITLE" = "Documents";
|
||||
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
|
||||
|
@ -476,6 +477,7 @@
|
|||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"EMOJI_REACTS_SHOW_LESS" = "Show less";
|
||||
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
|
||||
/* New conversation screen*/
|
||||
"vc_new_conversation_title" = "New Conversation";
|
||||
|
|
|
@ -450,6 +450,7 @@
|
|||
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
|
||||
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
|
||||
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
|
||||
"QUOTED_MESSAGE_NOT_FOUND" = "Original message not found.";
|
||||
"MEDIA_TAB_TITLE" = "Media";
|
||||
"DOCUMENT_TAB_TITLE" = "Documents";
|
||||
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
|
||||
|
@ -476,6 +477,7 @@
|
|||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"EMOJI_REACTS_SHOW_LESS" = "Show less";
|
||||
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
|
||||
/* New conversation screen*/
|
||||
"vc_new_conversation_title" = "New Conversation";
|
||||
|
|
|
@ -450,6 +450,7 @@
|
|||
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
|
||||
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
|
||||
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
|
||||
"QUOTED_MESSAGE_NOT_FOUND" = "Original message not found.";
|
||||
"MEDIA_TAB_TITLE" = "Media";
|
||||
"DOCUMENT_TAB_TITLE" = "Documents";
|
||||
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
|
||||
|
@ -476,6 +477,7 @@
|
|||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"EMOJI_REACTS_SHOW_LESS" = "Show less";
|
||||
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
|
||||
/* New conversation screen*/
|
||||
"vc_new_conversation_title" = "New Conversation";
|
||||
|
|
|
@ -450,6 +450,7 @@
|
|||
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
|
||||
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
|
||||
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
|
||||
"QUOTED_MESSAGE_NOT_FOUND" = "Original message not found.";
|
||||
"MEDIA_TAB_TITLE" = "Media";
|
||||
"DOCUMENT_TAB_TITLE" = "Documents";
|
||||
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
|
||||
|
@ -476,6 +477,7 @@
|
|||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"EMOJI_REACTS_SHOW_LESS" = "Show less";
|
||||
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
|
||||
/* New conversation screen*/
|
||||
"vc_new_conversation_title" = "New Conversation";
|
||||
|
|
|
@ -450,6 +450,7 @@
|
|||
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
|
||||
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
|
||||
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
|
||||
"QUOTED_MESSAGE_NOT_FOUND" = "Original message not found.";
|
||||
"MEDIA_TAB_TITLE" = "Media";
|
||||
"DOCUMENT_TAB_TITLE" = "Documents";
|
||||
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
|
||||
|
@ -476,6 +477,7 @@
|
|||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"EMOJI_REACTS_SHOW_LESS" = "Show less";
|
||||
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
|
||||
/* New conversation screen*/
|
||||
"vc_new_conversation_title" = "New Conversation";
|
||||
|
|
|
@ -450,6 +450,7 @@
|
|||
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
|
||||
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
|
||||
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
|
||||
"QUOTED_MESSAGE_NOT_FOUND" = "Original message not found.";
|
||||
"MEDIA_TAB_TITLE" = "Media";
|
||||
"DOCUMENT_TAB_TITLE" = "Documents";
|
||||
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
|
||||
|
@ -476,6 +477,7 @@
|
|||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"EMOJI_REACTS_SHOW_LESS" = "Show less";
|
||||
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
|
||||
/* New conversation screen*/
|
||||
"vc_new_conversation_title" = "New Conversation";
|
||||
|
|
|
@ -450,6 +450,7 @@
|
|||
"SEND_FAILED_NOTIFICATION_BODY" = "Your message failed to send.";
|
||||
"INVALID_SESSION_ID_MESSAGE" = "Please check the Session ID and try again.";
|
||||
"INVALID_RECOVERY_PHRASE_MESSAGE" = "Please check the Recovery Phrase and try again.";
|
||||
"QUOTED_MESSAGE_NOT_FOUND" = "Original message not found.";
|
||||
"MEDIA_TAB_TITLE" = "Media";
|
||||
"DOCUMENT_TAB_TITLE" = "Documents";
|
||||
"DOCUMENT_TILES_EMPTY_DOCUMENT" = "You don't have any document in this conversation.";
|
||||
|
@ -476,6 +477,7 @@
|
|||
"EMOJI_REACTS_NOTIFICATION" = "%@ reacts to a message with %@.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_ONE" = "And 1 other has reacted %@ to this message.";
|
||||
"EMOJI_REACTS_MORE_REACTORS_MUTIPLE" = "And %@ others have reacted %@ to this message.";
|
||||
"EMOJI_REACTS_SHOW_LESS" = "Show less";
|
||||
"EMOJI_REACTS_RATE_LIMIT_TOAST" = "Slow down! You've sent too many emoji reacts. Try again soon.";
|
||||
/* New conversation screen*/
|
||||
"vc_new_conversation_title" = "New Conversation";
|
||||
|
|
|
@ -26,7 +26,7 @@ final class FakeChatView: UIView {
|
|||
return result
|
||||
}()
|
||||
|
||||
private static let bubbleWidth = CGFloat(224)
|
||||
private static let bubbleWidth = UIDevice.current.isIPad ? CGFloat(468) : CGFloat(224)
|
||||
private static let bubbleCornerRadius = CGFloat(10)
|
||||
private static let startDelay: TimeInterval = 1
|
||||
private static let animationDuration: TimeInterval = 0.4
|
||||
|
@ -50,12 +50,12 @@ final class FakeChatView: UIView {
|
|||
stackView.axis = .vertical
|
||||
stackView.spacing = spacing
|
||||
stackView.alignment = .fill
|
||||
stackView.set(.width, to: UIScreen.main.bounds.width)
|
||||
let vInset = Values.smallSpacing
|
||||
stackView.layoutMargins = UIEdgeInsets(top: vInset, leading: Values.veryLargeSpacing, bottom: vInset, trailing: Values.veryLargeSpacing)
|
||||
stackView.isLayoutMarginsRelativeArrangement = true
|
||||
scrollView.addSubview(stackView)
|
||||
stackView.pin(to: scrollView)
|
||||
stackView.set(.width, to: .width, of: scrollView)
|
||||
addSubview(scrollView)
|
||||
scrollView.pin(to: self)
|
||||
let height = chatBubbles.reduce(0) { $0 + $1.systemLayoutSizeFitting(UIView.layoutFittingExpandedSize).height } + CGFloat(chatBubbles.count - 1) * spacing + 2 * vInset
|
||||
|
@ -65,7 +65,7 @@ final class FakeChatView: UIView {
|
|||
private func getChatBubble(withText text: String, wasSentByCurrentUser: Bool) -> UIView {
|
||||
let result = UIView()
|
||||
let bubbleView = UIView()
|
||||
bubbleView.set(.width, to: FakeChatView.bubbleWidth)
|
||||
bubbleView.set(.width, lessThanOrEqualTo: FakeChatView.bubbleWidth)
|
||||
bubbleView.themeShadowColor = .black
|
||||
bubbleView.layer.cornerRadius = FakeChatView.bubbleCornerRadius
|
||||
bubbleView.layer.shadowRadius = (ThemeManager.currentTheme.interfaceStyle == .light ? 4 : 8)
|
||||
|
|
|
@ -84,10 +84,22 @@ final class JoinOpenGroupVC: BaseVC, UIPageViewControllerDataSource, UIPageViewC
|
|||
pageVCView.pin(.top, to: .bottom, of: tabBar)
|
||||
pageVCView.pin(.trailing, to: .trailing, of: view)
|
||||
pageVCView.pin(.bottom, to: .bottom, of: view)
|
||||
|
||||
let navBarHeight: CGFloat = (navigationController?.navigationBar.frame.size.height ?? 0)
|
||||
let height: CGFloat = ((navigationController?.view.bounds.height ?? 0) - navBarHeight - TabBar.snHeight)
|
||||
enterURLVC.constrainHeight(to: height)
|
||||
scanQRCodePlaceholderVC.constrainHeight(to: height)
|
||||
let statusBarHeight: CGFloat = UIApplication.shared.statusBarFrame.size.height
|
||||
let height: CGFloat = ((navigationController?.view.bounds.height ?? 0) - navBarHeight - TabBar.snHeight - statusBarHeight)
|
||||
let size: CGSize = CGSize(width: UIScreen.main.bounds.width, height: height)
|
||||
enterURLVC.constrainSize(to: size)
|
||||
scanQRCodePlaceholderVC.constrainSize(to: size)
|
||||
}
|
||||
|
||||
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
super.viewWillTransition(to: size, with: coordinator)
|
||||
let height: CGFloat = (size.height - TabBar.snHeight)
|
||||
let size: CGSize = CGSize(width: size.width, height: height)
|
||||
enterURLVC.constrainSize(to: size)
|
||||
scanQRCodePlaceholderVC.constrainSize(to: size)
|
||||
enterURLVC.suggestionGrid.refreshLayout(with: size.width - 2 * Values.largeSpacing)
|
||||
}
|
||||
|
||||
// MARK: - General
|
||||
|
@ -243,13 +255,16 @@ private final class EnterURLVC: UIViewController, UIGestureRecognizerDelegate, O
|
|||
return result
|
||||
}()
|
||||
|
||||
private lazy var suggestionGrid: OpenGroupSuggestionGrid = {
|
||||
lazy var suggestionGrid: OpenGroupSuggestionGrid = {
|
||||
let maxWidth: CGFloat = (UIScreen.main.bounds.width - Values.largeSpacing * 2)
|
||||
let result: OpenGroupSuggestionGrid = OpenGroupSuggestionGrid(maxWidth: maxWidth)
|
||||
result.delegate = self
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private var viewWidth: NSLayoutConstraint?
|
||||
private var viewHeight: NSLayoutConstraint?
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
|
@ -298,7 +313,7 @@ private final class EnterURLVC: UIViewController, UIGestureRecognizerDelegate, O
|
|||
bottomConstraint = view.pin(.bottom, to: .bottom, of: stackView, withInset: bottomMargin)
|
||||
|
||||
// Constraints
|
||||
view.set(.width, to: UIScreen.main.bounds.width)
|
||||
viewWidth = view.set(.width, to: UIScreen.main.bounds.width)
|
||||
|
||||
// Dismiss keyboard on tap
|
||||
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
|
||||
|
@ -325,8 +340,18 @@ private final class EnterURLVC: UIViewController, UIGestureRecognizerDelegate, O
|
|||
|
||||
// MARK: - General
|
||||
|
||||
func constrainHeight(to height: CGFloat) {
|
||||
view.set(.height, to: height)
|
||||
func constrainSize(to size: CGSize) {
|
||||
if viewWidth == nil {
|
||||
viewWidth = view.set(.width, to: size.width)
|
||||
} else {
|
||||
viewWidth?.constant = size.width
|
||||
}
|
||||
|
||||
if viewHeight == nil {
|
||||
viewHeight = view.set(.height, to: size.height)
|
||||
} else {
|
||||
viewHeight?.constant = size.height
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func dismissKeyboard() {
|
||||
|
@ -438,6 +463,9 @@ private final class EnterURLVC: UIViewController, UIGestureRecognizerDelegate, O
|
|||
private final class ScanQRCodePlaceholderVC: UIViewController {
|
||||
weak var joinOpenGroupVC: JoinOpenGroupVC?
|
||||
|
||||
private var viewWidth: NSLayoutConstraint?
|
||||
private var viewHeight: NSLayoutConstraint?
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
override func viewDidLoad() {
|
||||
|
@ -467,7 +495,6 @@ private final class ScanQRCodePlaceholderVC: UIViewController {
|
|||
stackView.alignment = .center
|
||||
|
||||
// Constraints
|
||||
view.set(.width, to: UIScreen.main.bounds.width)
|
||||
view.addSubview(stackView)
|
||||
stackView.pin(.leading, to: .leading, of: view, withInset: Values.massiveSpacing)
|
||||
view.pin(.trailing, to: .trailing, of: stackView, withInset: Values.massiveSpacing)
|
||||
|
@ -476,8 +503,18 @@ private final class ScanQRCodePlaceholderVC: UIViewController {
|
|||
verticalCenteringConstraint.constant = -16 // Makes things appear centered visually
|
||||
}
|
||||
|
||||
func constrainHeight(to height: CGFloat) {
|
||||
view.set(.height, to: height)
|
||||
func constrainSize(to size: CGSize) {
|
||||
if viewWidth == nil {
|
||||
viewWidth = view.set(.width, to: size.width)
|
||||
} else {
|
||||
viewWidth?.constant = size.width
|
||||
}
|
||||
|
||||
if viewHeight == nil {
|
||||
viewHeight = view.set(.height, to: size.height)
|
||||
} else {
|
||||
viewHeight?.constant = size.height
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func requestCameraAccess() {
|
||||
|
|
|
@ -5,7 +5,7 @@ import SessionUIKit
|
|||
|
||||
final class OpenGroupSuggestionGrid: UIView, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
|
||||
private let itemsPerSection: Int = (UIDevice.current.isIPad ? 4 : 2)
|
||||
private let maxWidth: CGFloat
|
||||
private var maxWidth: CGFloat
|
||||
private var rooms: [OpenGroupAPI.Room] = [] { didSet { update() } }
|
||||
private var heightConstraint: NSLayoutConstraint!
|
||||
|
||||
|
@ -162,6 +162,11 @@ final class OpenGroupSuggestionGrid: UIView, UICollectionViewDataSource, UIColle
|
|||
errorView.isHidden = (roomCount > 0)
|
||||
}
|
||||
|
||||
public func refreshLayout(with maxWidth: CGFloat) {
|
||||
self.maxWidth = maxWidth
|
||||
collectionView.collectionViewLayout.invalidateLayout()
|
||||
}
|
||||
|
||||
// MARK: - Layout
|
||||
|
||||
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
|
||||
|
|
|
@ -236,9 +236,14 @@ private final class ViewMyQRCodeVC : UIViewController {
|
|||
let shareButtonContainer = UIView()
|
||||
shareButtonContainer.addSubview(shareButton)
|
||||
shareButton.pin(.top, to: .top, of: shareButtonContainer)
|
||||
shareButton.pin(.leading, to: .leading, of: shareButtonContainer, withInset: 80)
|
||||
shareButton.pin(.trailing, to: .trailing, of: shareButtonContainer, withInset: -80)
|
||||
shareButton.pin(.bottom, to: .bottom, of: shareButtonContainer)
|
||||
if UIDevice.current.isIPad {
|
||||
shareButton.center(in: shareButtonContainer)
|
||||
shareButton.set(.width, to: Values.iPadButtonWidth)
|
||||
} else {
|
||||
shareButton.pin(.leading, to: .leading, of: shareButtonContainer, withInset: 80)
|
||||
shareButton.pin(.trailing, to: .trailing, of: shareButtonContainer, withInset: -80)
|
||||
}
|
||||
|
||||
// Set up stack view
|
||||
let spacing = (isIPhone5OrSmaller ? Values.mediumSpacing : Values.largeSpacing)
|
||||
|
|
|
@ -190,7 +190,7 @@ public final class FullConversationCell: UITableViewCell {
|
|||
|
||||
let labelContainerView = UIStackView(arrangedSubviews: [ topLabelStackView, bottomLabelStackView ])
|
||||
labelContainerView.axis = .vertical
|
||||
labelContainerView.alignment = .leading
|
||||
labelContainerView.alignment = .fill
|
||||
labelContainerView.spacing = 6
|
||||
labelContainerView.isUserInteractionEnabled = false
|
||||
|
||||
|
@ -206,12 +206,10 @@ public final class FullConversationCell: UITableViewCell {
|
|||
accentLineView.pin(.bottom, to: .bottom, of: contentView)
|
||||
timestampLabel.setContentCompressionResistancePriority(.required, for: NSLayoutConstraint.Axis.horizontal)
|
||||
|
||||
// HACK: The six lines below are part of a workaround for a weird layout bug
|
||||
topLabelStackView.set(.width, to: UIScreen.main.bounds.width - Values.accentLineThickness - profilePictureViewSize - 3 * Values.mediumSpacing)
|
||||
// HACK: The 4 lines below are part of a workaround for a weird layout bug
|
||||
topLabelStackView.set(.height, to: 20)
|
||||
topLabelSpacer.set(.height, to: 20)
|
||||
|
||||
bottomLabelStackView.set(.width, to: UIScreen.main.bounds.width - Values.accentLineThickness - profilePictureViewSize - 3 * Values.mediumSpacing)
|
||||
bottomLabelStackView.set(.height, to: 18)
|
||||
bottomLabelSpacer.set(.height, to: 18)
|
||||
|
||||
|
@ -223,12 +221,8 @@ public final class FullConversationCell: UITableViewCell {
|
|||
typingIndicatorView.pin(.leading, to: .leading, of: snippetLabelContainer)
|
||||
typingIndicatorView.centerYAnchor.constraint(equalTo: snippetLabel.centerYAnchor).isActive = true
|
||||
|
||||
stackView.pin(.leading, to: .leading, of: contentView)
|
||||
stackView.pin(.top, to: .top, of: contentView)
|
||||
|
||||
// HACK: The two lines below are part of a workaround for a weird layout bug
|
||||
stackView.set(.width, to: UIScreen.main.bounds.width - Values.mediumSpacing)
|
||||
stackView.set(.height, to: cellHeight)
|
||||
stackView.pin([ UIView.VerticalEdge.bottom, UIView.VerticalEdge.top, UIView.HorizontalEdge.leading ], to: contentView)
|
||||
stackView.pin(.trailing, to: .trailing, of: contentView, withInset: -Values.mediumSpacing)
|
||||
}
|
||||
|
||||
// MARK: - Content
|
||||
|
|
|
@ -10,8 +10,6 @@ final class ScanQRCodeWrapperVC: BaseVC {
|
|||
private let message: String?
|
||||
private let scanQRCodeVC = QRCodeScanningViewController()
|
||||
|
||||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask { return .portrait }
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
init(message: String?) {
|
||||
|
@ -47,7 +45,7 @@ final class ScanQRCodeWrapperVC: BaseVC {
|
|||
scanQRCodeVCView.autoPinEdge(.top, to: .top, of: view)
|
||||
|
||||
if let message = message {
|
||||
scanQRCodeVCView.autoPinToSquareAspectRatio()
|
||||
scanQRCodeVCView.set(.height, lessThanOrEqualTo: UIScreen.main.bounds.width)
|
||||
|
||||
// Set up bottom view
|
||||
let bottomView = UIView()
|
||||
|
@ -78,8 +76,6 @@ final class ScanQRCodeWrapperVC: BaseVC {
|
|||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
UIDevice.current.ows_setOrientation(.portrait)
|
||||
|
||||
self.scanQRCodeVC.startCapture()
|
||||
}
|
||||
|
||||
|
|
|
@ -114,17 +114,7 @@ class SessionAvatarCell: UITableViewCell {
|
|||
stackView.alignment = .center
|
||||
stackView.distribution = .fillEqually
|
||||
stackView.spacing = (UIDevice.current.isIPad ? Values.iPadButtonSpacing : Values.mediumSpacing)
|
||||
|
||||
if (UIDevice.current.isIPad) {
|
||||
stackView.layoutMargins = UIEdgeInsets(
|
||||
top: 0,
|
||||
left: Values.iPadButtonContainerMargin,
|
||||
bottom: 0,
|
||||
right: Values.iPadButtonContainerMargin
|
||||
)
|
||||
stackView.isLayoutMarginsRelativeArrangement = true
|
||||
}
|
||||
|
||||
|
||||
return stackView
|
||||
}()
|
||||
|
||||
|
@ -242,6 +232,10 @@ class SessionAvatarCell: UITableViewCell {
|
|||
descriptionSeparator.update(title: style.separatorTitle)
|
||||
descriptionSeparator.isHidden = (style.separatorTitle == nil)
|
||||
|
||||
if (UIDevice.current.isIPad) {
|
||||
descriptionActionStackView.addArrangedSubview(UIView.hStretchingSpacer())
|
||||
}
|
||||
|
||||
style.descriptionActions.forEach { action in
|
||||
let result: SessionButton = SessionButton(style: .bordered, size: .medium)
|
||||
result.setTitle(action.title, for: UIControl.State.normal)
|
||||
|
@ -252,6 +246,10 @@ class SessionAvatarCell: UITableViewCell {
|
|||
|
||||
descriptionActionStackView.addArrangedSubview(result)
|
||||
}
|
||||
|
||||
if (UIDevice.current.isIPad) {
|
||||
descriptionActionStackView.addArrangedSubview(UIView.hStretchingSpacer())
|
||||
}
|
||||
descriptionActionStackView.isHidden = style.descriptionActions.isEmpty
|
||||
}
|
||||
|
||||
|
|
|
@ -219,6 +219,10 @@ public extension OpenGroup {
|
|||
// Always force the server to lowercase
|
||||
return "\(server.lowercased()).\(roomToken)"
|
||||
}
|
||||
|
||||
static func urlFor(server: String, roomToken: String, publicKey: String) -> String {
|
||||
return "\(server)/\(roomToken)?public_key=\(publicKey)"
|
||||
}
|
||||
}
|
||||
|
||||
extension OpenGroup: CustomStringConvertible, CustomDebugStringConvertible {
|
||||
|
@ -243,51 +247,3 @@ extension OpenGroup: CustomStringConvertible, CustomDebugStringConvertible {
|
|||
].joined(separator: ", ")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Objective-C Support
|
||||
|
||||
// TODO: Remove this when possible
|
||||
|
||||
@objc(SMKOpenGroup)
|
||||
public class SMKOpenGroup: NSObject {
|
||||
@objc(inviteUsers:toOpenGroupFor:)
|
||||
public static func invite(selectedUsers: Set<String>, openGroupThreadId: String) {
|
||||
Storage.shared.write { db in
|
||||
guard let openGroup: OpenGroup = try OpenGroup.fetchOne(db, id: openGroupThreadId) else { return }
|
||||
|
||||
let urlString: String = "\(openGroup.server)/\(openGroup.roomToken)?public_key=\(openGroup.publicKey)"
|
||||
|
||||
try selectedUsers.forEach { userId in
|
||||
let thread: SessionThread = try SessionThread.fetchOrCreate(db, id: userId, variant: .contact)
|
||||
|
||||
try LinkPreview(
|
||||
url: urlString,
|
||||
variant: .openGroupInvitation,
|
||||
title: openGroup.name
|
||||
)
|
||||
.save(db)
|
||||
|
||||
let interaction: Interaction = try Interaction(
|
||||
threadId: thread.id,
|
||||
authorId: userId,
|
||||
variant: .standardOutgoing,
|
||||
timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000)),
|
||||
expiresInSeconds: try? DisappearingMessagesConfiguration
|
||||
.select(.durationSeconds)
|
||||
.filter(id: userId)
|
||||
.filter(DisappearingMessagesConfiguration.Columns.isEnabled == true)
|
||||
.asRequest(of: TimeInterval.self)
|
||||
.fetchOne(db),
|
||||
linkPreviewUrl: urlString
|
||||
)
|
||||
.inserted(db)
|
||||
|
||||
try MessageSender.send(
|
||||
db,
|
||||
interaction: interaction,
|
||||
in: thread
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -89,51 +89,7 @@ public extension Quote {
|
|||
self.interactionId = interactionId
|
||||
self.timestampMs = Int64(quoteProto.id)
|
||||
self.authorId = quoteProto.author
|
||||
|
||||
// Prefer to generate the text snippet locally if available.
|
||||
let quotedInteraction: Interaction? = try? thread
|
||||
.interactions
|
||||
.filter(Interaction.Columns.authorId == quoteProto.author)
|
||||
.filter(Interaction.Columns.timestampMs == Double(quoteProto.id))
|
||||
.fetchOne(db)
|
||||
|
||||
if let quotedInteraction: Interaction = quotedInteraction, quotedInteraction.body?.isEmpty == false {
|
||||
self.body = quotedInteraction.body
|
||||
}
|
||||
else if let body: String = quoteProto.text, !body.isEmpty {
|
||||
self.body = body
|
||||
}
|
||||
else {
|
||||
self.body = nil
|
||||
}
|
||||
|
||||
// We only use the first attachment
|
||||
if let attachment = quoteProto.attachments.first(where: { $0.thumbnail != nil })?.thumbnail {
|
||||
self.attachmentId = try quotedInteraction
|
||||
.map { quotedInteraction -> Attachment? in
|
||||
// If the quotedInteraction has an attachment then try clone it
|
||||
if let attachment: Attachment = try? quotedInteraction.attachments.fetchOne(db) {
|
||||
return attachment.cloneAsQuoteThumbnail()
|
||||
}
|
||||
|
||||
// Otherwise if the quotedInteraction has a link preview, try clone that
|
||||
return try? quotedInteraction.linkPreview
|
||||
.fetchOne(db)?
|
||||
.attachment
|
||||
.fetchOne(db)?
|
||||
.cloneAsQuoteThumbnail()
|
||||
}
|
||||
.defaulting(to: Attachment(proto: attachment))
|
||||
.inserted(db)
|
||||
.id
|
||||
}
|
||||
else {
|
||||
self.attachmentId = nil
|
||||
}
|
||||
|
||||
// Make sure the quote is valid before completing
|
||||
if self.body == nil && self.attachmentId == nil {
|
||||
return nil
|
||||
}
|
||||
self.body = nil
|
||||
self.attachmentId = nil
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,7 +42,13 @@ extension ConfigurationMessage {
|
|||
.filter(OpenGroup.Columns.roomToken != "")
|
||||
.filter(OpenGroup.Columns.isActive)
|
||||
.fetchAll(db)
|
||||
.map { "\($0.server)/\($0.roomToken)?public_key=\($0.publicKey)" }
|
||||
.map { openGroup in
|
||||
OpenGroup.urlFor(
|
||||
server: openGroup.server,
|
||||
roomToken: openGroup.roomToken,
|
||||
publicKey: openGroup.publicKey
|
||||
)
|
||||
}
|
||||
.asSet()
|
||||
let contacts: Set<CMContact> = try Contact
|
||||
.filter(Contact.Columns.id != currentUserProfile.id)
|
||||
|
|
|
@ -633,6 +633,7 @@ public extension MessageViewModel {
|
|||
let disappearingMessagesConfig: TypedTableAlias<DisappearingMessagesConfiguration> = TypedTableAlias()
|
||||
let profile: TypedTableAlias<Profile> = TypedTableAlias()
|
||||
let quote: TypedTableAlias<Quote> = TypedTableAlias()
|
||||
let interactionAttachment: TypedTableAlias<InteractionAttachment> = TypedTableAlias()
|
||||
let linkPreview: TypedTableAlias<LinkPreview> = TypedTableAlias()
|
||||
|
||||
let threadProfileTableLiteral: SQL = SQL(stringLiteral: "threadProfile")
|
||||
|
@ -708,7 +709,19 @@ public extension MessageViewModel {
|
|||
LEFT JOIN \(DisappearingMessagesConfiguration.self) ON \(disappearingMessagesConfig[.threadId]) = \(interaction[.threadId])
|
||||
LEFT JOIN \(OpenGroup.self) ON \(openGroup[.threadId]) = \(interaction[.threadId])
|
||||
LEFT JOIN \(Profile.self) ON \(profile[.id]) = \(interaction[.authorId])
|
||||
LEFT JOIN \(Quote.self) ON \(quote[.interactionId]) = \(interaction[.id])
|
||||
LEFT JOIN (
|
||||
SELECT \(quote[.interactionId]),
|
||||
\(quote[.authorId]),
|
||||
\(quote[.timestampMs]),
|
||||
\(interaction[.body]) AS \(Quote.Columns.body),
|
||||
\(interactionAttachment[.attachmentId]) AS \(Quote.Columns.attachmentId)
|
||||
FROM \(Quote.self)
|
||||
LEFT JOIN \(Interaction.self) ON (
|
||||
\(quote[.authorId]) = \(interaction[.authorId]) AND
|
||||
\(quote[.timestampMs]) = \(interaction[.timestampMs])
|
||||
)
|
||||
LEFT JOIN \(InteractionAttachment.self) ON \(interaction[.id]) = \(interactionAttachment[.interactionId])
|
||||
) AS \(ViewModel.quoteKey) ON \(quote[.interactionId]) = \(interaction[.id])
|
||||
LEFT JOIN \(Attachment.self) AS \(ViewModel.quoteAttachmentKey) ON \(ViewModel.quoteAttachmentKey).\(attachmentIdColumnLiteral) = \(quote[.attachmentId])
|
||||
LEFT JOIN \(LinkPreview.self) ON (
|
||||
\(linkPreview[.url]) = \(interaction[.linkPreviewUrl]) AND
|
||||
|
|
|
@ -43,6 +43,7 @@ public struct SessionThreadViewModel: FetchableRecordWithRowId, Decodable, Equat
|
|||
public static let openGroupNameKey: SQL = SQL(stringLiteral: CodingKeys.openGroupName.stringValue)
|
||||
public static let openGroupServerKey: SQL = SQL(stringLiteral: CodingKeys.openGroupServer.stringValue)
|
||||
public static let openGroupRoomTokenKey: SQL = SQL(stringLiteral: CodingKeys.openGroupRoomToken.stringValue)
|
||||
public static let openGroupPublicKeyKey: SQL = SQL(stringLiteral: CodingKeys.openGroupPublicKey.stringValue)
|
||||
public static let openGroupProfilePictureDataKey: SQL = SQL(stringLiteral: CodingKeys.openGroupProfilePictureData.stringValue)
|
||||
public static let openGroupUserCountKey: SQL = SQL(stringLiteral: CodingKeys.openGroupUserCount.stringValue)
|
||||
public static let openGroupPermissionsKey: SQL = SQL(stringLiteral: CodingKeys.openGroupPermissions.stringValue)
|
||||
|
@ -117,6 +118,7 @@ public struct SessionThreadViewModel: FetchableRecordWithRowId, Decodable, Equat
|
|||
public let openGroupName: String?
|
||||
public let openGroupServer: String?
|
||||
public let openGroupRoomToken: String?
|
||||
public let openGroupPublicKey: String?
|
||||
public let openGroupProfilePictureData: Data?
|
||||
private let openGroupUserCount: Int?
|
||||
private let openGroupPermissions: OpenGroup.Permissions?
|
||||
|
@ -274,6 +276,7 @@ public extension SessionThreadViewModel {
|
|||
self.openGroupName = nil
|
||||
self.openGroupServer = nil
|
||||
self.openGroupRoomToken = nil
|
||||
self.openGroupPublicKey = nil
|
||||
self.openGroupProfilePictureData = nil
|
||||
self.openGroupUserCount = nil
|
||||
self.openGroupPermissions = nil
|
||||
|
@ -334,6 +337,7 @@ public extension SessionThreadViewModel {
|
|||
openGroupName: self.openGroupName,
|
||||
openGroupServer: self.openGroupServer,
|
||||
openGroupRoomToken: self.openGroupRoomToken,
|
||||
openGroupPublicKey: self.openGroupPublicKey,
|
||||
openGroupProfilePictureData: self.openGroupProfilePictureData,
|
||||
openGroupUserCount: self.openGroupUserCount,
|
||||
openGroupPermissions: self.openGroupPermissions,
|
||||
|
@ -387,6 +391,7 @@ public extension SessionThreadViewModel {
|
|||
openGroupName: self.openGroupName,
|
||||
openGroupServer: self.openGroupServer,
|
||||
openGroupRoomToken: self.openGroupRoomToken,
|
||||
openGroupPublicKey: self.openGroupPublicKey,
|
||||
openGroupProfilePictureData: self.openGroupProfilePictureData,
|
||||
openGroupUserCount: self.openGroupUserCount,
|
||||
openGroupPermissions: self.openGroupPermissions,
|
||||
|
@ -753,6 +758,7 @@ public extension SessionThreadViewModel {
|
|||
\(openGroup[.name]) AS \(ViewModel.openGroupNameKey),
|
||||
\(openGroup[.server]) AS \(ViewModel.openGroupServerKey),
|
||||
\(openGroup[.roomToken]) AS \(ViewModel.openGroupRoomTokenKey),
|
||||
\(openGroup[.publicKey]) AS \(ViewModel.openGroupPublicKeyKey),
|
||||
\(openGroup[.userCount]) AS \(ViewModel.openGroupUserCountKey),
|
||||
\(openGroup[.permissions]) AS \(ViewModel.openGroupPermissionsKey),
|
||||
|
||||
|
@ -847,6 +853,9 @@ public extension SessionThreadViewModel {
|
|||
\(closedGroup[.name]) AS \(ViewModel.closedGroupNameKey),
|
||||
(\(groupMember[.profileId]) IS NOT NULL) AS \(ViewModel.currentUserIsClosedGroupMemberKey),
|
||||
\(openGroup[.name]) AS \(ViewModel.openGroupNameKey),
|
||||
\(openGroup[.server]) AS \(ViewModel.openGroupServerKey),
|
||||
\(openGroup[.roomToken]) AS \(ViewModel.openGroupRoomTokenKey),
|
||||
\(openGroup[.publicKey]) AS \(ViewModel.openGroupPublicKeyKey),
|
||||
\(openGroup[.imageData]) AS \(ViewModel.openGroupProfilePictureDataKey),
|
||||
|
||||
\(SQL("\(userPublicKey)")) AS \(ViewModel.currentUserPublicKeyKey)
|
||||
|
|
|
@ -79,6 +79,32 @@ class OpenGroupSpec: QuickSpec {
|
|||
.to(equal("OpenGroup(server: \"server\", roomToken: \"room\", id: \"server.room\", publicKey: \"1234\", isActive: true, name: \"name\", roomDescription: null, imageId: null, userCount: 0, infoUpdates: 0, sequenceNumber: 0, inboxLatestMessageId: 0, outboxLatestMessageId: 0, pollFailureCount: 0, permissions: ---)"))
|
||||
}
|
||||
}
|
||||
|
||||
context("when generating an id") {
|
||||
it("generates correctly") {
|
||||
expect(OpenGroup.idFor(roomToken: "room", server: "server")).to(equal("server.room"))
|
||||
}
|
||||
|
||||
it("converts the server to lowercase") {
|
||||
expect(OpenGroup.idFor(roomToken: "room", server: "SeRVeR")).to(equal("server.room"))
|
||||
}
|
||||
|
||||
it("maintains the casing of the roomToken") {
|
||||
expect(OpenGroup.idFor(roomToken: "RoOM", server: "server")).to(equal("server.RoOM"))
|
||||
}
|
||||
}
|
||||
|
||||
context("when generating a url") {
|
||||
it("generates the url correctly") {
|
||||
expect(OpenGroup.urlFor(server: "server", roomToken: "room", publicKey: "key"))
|
||||
.to(equal("server/room?public_key=key"))
|
||||
}
|
||||
|
||||
it("maintains the casing provided") {
|
||||
expect(OpenGroup.urlFor(server: "SeRVer", roomToken: "RoOM", publicKey: "KEy"))
|
||||
.to(equal("SeRVer/RoOM?public_key=KEy"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ public final class Separator: UIView {
|
|||
|
||||
private lazy var titleLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.setContentCompressionResistancePriority(.required, for: .vertical)
|
||||
result.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
result.themeTextColor = .textSecondary
|
||||
result.textAlignment = .center
|
||||
|
|
|
@ -57,5 +57,4 @@ public final class Values : NSObject {
|
|||
@objc public static let iPadButtonWidth = CGFloat(196)
|
||||
@objc public static let iPadButtonSpacing = CGFloat(32)
|
||||
@objc public static let iPadUserSessionIdContainerWidth = iPadButtonWidth * 2 + iPadButtonSpacing
|
||||
@objc public static let iPadButtonContainerMargin = (UIScreen.main.bounds.width - iPadButtonSpacing) / 2 - iPadButtonWidth - largeSpacing
|
||||
}
|
||||
|
|
|
@ -176,4 +176,17 @@ public extension UIView {
|
|||
constraint.isActive = true
|
||||
return constraint
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func set(_ dimension: Dimension, lessThanOrEqualTo size: CGFloat) -> NSLayoutConstraint {
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
let constraint: NSLayoutConstraint = {
|
||||
switch dimension {
|
||||
case .width: return widthAnchor.constraint(lessThanOrEqualToConstant: size)
|
||||
case .height: return heightAnchor.constraint(lessThanOrEqualToConstant: size)
|
||||
}
|
||||
}()
|
||||
constraint.isActive = true
|
||||
return constraint
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,8 @@ public extension Database {
|
|||
}
|
||||
|
||||
func interrupt() {
|
||||
guard sqliteConnection != nil else { return }
|
||||
|
||||
sqlite3_interrupt(sqliteConnection)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue