Merge pull request #739 from oxen-io/dev

2.2.3
This commit is contained in:
RyanZhao 2022-11-21 11:01:06 +11:00 committed by GitHub
commit 27f273e823
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
55 changed files with 384 additions and 200 deletions

View File

@ -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;

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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:

View File

@ -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

View File

@ -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()

View File

@ -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()
}

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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
}

View File

@ -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>

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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" = "مکالمه جدید";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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";

View File

@ -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)

View File

@ -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() {

View File

@ -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 {

View File

@ -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)

View File

@ -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

View File

@ -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()
}

View File

@ -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
}

View File

@ -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
)
}
}
}
}

View File

@ -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
}
}

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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"))
}
}
}
}
}

View File

@ -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

View File

@ -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
}

View File

@ -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
}
}

View File

@ -32,6 +32,8 @@ public extension Database {
}
func interrupt() {
guard sqliteConnection != nil else { return }
sqlite3_interrupt(sqliteConnection)
}
}