commit
9a4988f212
|
@ -7212,7 +7212,7 @@
|
|||
"$(SRCROOT)",
|
||||
);
|
||||
LLVM_LTO = NO;
|
||||
MARKETING_VERSION = 2.2.4;
|
||||
MARKETING_VERSION = 2.2.5;
|
||||
OTHER_LDFLAGS = "$(inherited)";
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
|
||||
|
@ -7284,7 +7284,7 @@
|
|||
"$(SRCROOT)",
|
||||
);
|
||||
LLVM_LTO = NO;
|
||||
MARKETING_VERSION = 2.2.4;
|
||||
MARKETING_VERSION = 2.2.5;
|
||||
OTHER_LDFLAGS = "$(inherited)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
|
||||
PRODUCT_NAME = Session;
|
||||
|
|
|
@ -206,7 +206,7 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate {
|
|||
let thread: SessionThread = try? SessionThread.fetchOne(db, id: sessionId)
|
||||
else { return }
|
||||
|
||||
let timestampMs: Int64 = Int64(floor(Date().timeIntervalSince1970 * 1000))
|
||||
let timestampMs: Int64 = SnodeAPI.currentOffsetTimestampMs()
|
||||
let message: CallMessage = CallMessage(
|
||||
uuid: self.uuid,
|
||||
kind: .preOffer,
|
||||
|
|
|
@ -112,6 +112,7 @@ final class CallVC: UIViewController, VideoPreviewDelegate {
|
|||
|
||||
private lazy var hangUpButton: UIButton = {
|
||||
let result = UIButton(type: .custom)
|
||||
result.accessibilityLabel = "End call button"
|
||||
result.setImage(
|
||||
UIImage(named: "EndCall")?
|
||||
.withRenderingMode(.alwaysTemplate),
|
||||
|
|
|
@ -13,6 +13,8 @@ final class EditClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegat
|
|||
let profileId: String
|
||||
let role: GroupMember.Role
|
||||
let profile: Profile?
|
||||
let accessibilityLabel: String?
|
||||
let accessibilityId: String?
|
||||
}
|
||||
|
||||
private let threadId: String
|
||||
|
@ -30,6 +32,8 @@ final class EditClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegat
|
|||
|
||||
private lazy var groupNameLabel: UILabel = {
|
||||
let result: UILabel = UILabel()
|
||||
result.accessibilityLabel = "Group name"
|
||||
result.isAccessibilityElement = true
|
||||
result.font = .boldSystemFont(ofSize: Values.veryLargeFontSize)
|
||||
result.themeTextColor = .textPrimary
|
||||
result.lineBreakMode = .byTruncatingTail
|
||||
|
@ -44,12 +48,16 @@ final class EditClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegat
|
|||
usesDefaultHeight: false
|
||||
)
|
||||
result.textAlignment = .center
|
||||
result.isAccessibilityElement = true
|
||||
result.accessibilityIdentifier = "Group name text field"
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var addMembersButton: SessionButton = {
|
||||
let result: SessionButton = SessionButton(style: .bordered, size: .medium)
|
||||
result.accessibilityLabel = "Add members"
|
||||
result.isAccessibilityElement = true
|
||||
result.setTitle("vc_conversation_settings_invite_button_title".localized(), for: .normal)
|
||||
result.addTarget(self, action: #selector(addMembers), for: UIControl.Event.touchUpInside)
|
||||
result.contentEdgeInsets = UIEdgeInsets(top: 0, leading: Values.mediumSpacing, bottom: 0, trailing: Values.mediumSpacing)
|
||||
|
@ -59,6 +67,9 @@ final class EditClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegat
|
|||
|
||||
@objc private lazy var tableView: UITableView = {
|
||||
let result: UITableView = UITableView()
|
||||
result.accessibilityLabel = "Contact"
|
||||
result.accessibilityIdentifier = "Contact"
|
||||
result.isAccessibilityElement = true
|
||||
result.dataSource = self
|
||||
result.delegate = self
|
||||
result.separatorStyle = .none
|
||||
|
@ -264,6 +275,12 @@ final class EditClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegat
|
|||
}
|
||||
|
||||
let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(handleDoneButtonTapped))
|
||||
if isEditingGroupName {
|
||||
doneButton.accessibilityLabel = "Accept name change"
|
||||
}
|
||||
else {
|
||||
doneButton.accessibilityLabel = "Apply changes"
|
||||
}
|
||||
doneButton.themeTintColor = .textPrimary
|
||||
navigationItem.rightBarButtonItem = doneButton
|
||||
}
|
||||
|
@ -343,7 +360,9 @@ final class EditClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegat
|
|||
GroupMemberDisplayInfo(
|
||||
profileId: profile.id,
|
||||
role: .standard,
|
||||
profile: profile
|
||||
profile: profile,
|
||||
accessibilityLabel: "Contact",
|
||||
accessibilityId: "Contact"
|
||||
)
|
||||
}
|
||||
self?.membersAndZombies = (self?.membersAndZombies ?? [])
|
||||
|
|
|
@ -9,15 +9,17 @@ import SessionMessagingKit
|
|||
import SignalUtilitiesKit
|
||||
|
||||
private protocol TableViewTouchDelegate {
|
||||
func tableViewWasTouched(_ tableView: TableView)
|
||||
func tableViewWasTouched(_ tableView: TableView, withView hitView: UIView?)
|
||||
}
|
||||
|
||||
private final class TableView: UITableView {
|
||||
var touchDelegate: TableViewTouchDelegate?
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
touchDelegate?.tableViewWasTouched(self)
|
||||
return super.hitTest(point, with: event)
|
||||
let resultingView: UIView? = super.hitTest(point, with: event)
|
||||
touchDelegate?.tableViewWasTouched(self, withView: resultingView)
|
||||
|
||||
return resultingView
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -48,6 +50,8 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate
|
|||
result.themeBorderColor = .borderSeparator
|
||||
result.layer.cornerRadius = 13
|
||||
result.delegate = self
|
||||
result.accessibilityIdentifier = "Group name input"
|
||||
result.isAccessibilityElement = true
|
||||
|
||||
return result
|
||||
}()
|
||||
|
@ -133,6 +137,8 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate
|
|||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.setTitle("CREATE_GROUP_BUTTON_TITLE".localized(), for: .normal)
|
||||
result.addTarget(self, action: #selector(createClosedGroup), for: .touchUpInside)
|
||||
result.accessibilityIdentifier = "Create group"
|
||||
result.isAccessibilityElement = true
|
||||
result.set(.width, to: 160)
|
||||
|
||||
return result
|
||||
|
@ -151,6 +157,8 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate
|
|||
let closeButton = UIBarButtonItem(image: #imageLiteral(resourceName: "X"), style: .plain, target: self, action: #selector(close))
|
||||
closeButton.themeTintColor = .textPrimary
|
||||
navigationItem.rightBarButtonItem = closeButton
|
||||
navigationItem.leftBarButtonItem?.accessibilityIdentifier = "Cancel"
|
||||
navigationItem.leftBarButtonItem?.isAccessibilityElement = true
|
||||
|
||||
// Set up content
|
||||
setUpViewHierarchy()
|
||||
|
@ -204,8 +212,9 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate
|
|||
title: profile.displayName(),
|
||||
rightAccessory: .radio(isSelected: { [weak self] in
|
||||
self?.selectedContacts.contains(profile.id) == true
|
||||
})
|
||||
),
|
||||
}),
|
||||
accessibilityIdentifier: "Contact"
|
||||
),
|
||||
style: .edgeToEdge,
|
||||
position: Position.with(indexPath.row, count: data[indexPath.section].elements.count)
|
||||
)
|
||||
|
@ -268,10 +277,23 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate
|
|||
)
|
||||
}
|
||||
|
||||
fileprivate func tableViewWasTouched(_ tableView: TableView) {
|
||||
fileprivate func tableViewWasTouched(_ tableView: TableView, withView hitView: UIView?) {
|
||||
if nameTextField.isFirstResponder {
|
||||
nameTextField.resignFirstResponder()
|
||||
}
|
||||
else if searchBar.isFirstResponder {
|
||||
var hitSuperview: UIView? = hitView?.superview
|
||||
|
||||
while hitSuperview != nil && hitSuperview != searchBar {
|
||||
hitSuperview = hitSuperview?.superview
|
||||
}
|
||||
|
||||
// If the user hit the cancel button then do nothing (we want to let the cancel
|
||||
// button remove the focus or it will instantly refocus)
|
||||
if hitSuperview == searchBar { return }
|
||||
|
||||
searchBar.resignFirstResponder()
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func close() {
|
||||
|
@ -286,6 +308,7 @@ final class NewClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegate
|
|||
explanation: message,
|
||||
cancelTitle: "BUTTON_OK".localized(),
|
||||
cancelStyle: .alert_text
|
||||
|
||||
)
|
||||
)
|
||||
present(modal, animated: true)
|
||||
|
|
|
@ -10,6 +10,7 @@ extension ContextMenuVC {
|
|||
let isEmojiAction: Bool
|
||||
let isEmojiPlus: Bool
|
||||
let isDismissAction: Bool
|
||||
let accessibilityLabel: String?
|
||||
let work: () -> Void
|
||||
|
||||
// MARK: - Initialization
|
||||
|
@ -20,6 +21,7 @@ extension ContextMenuVC {
|
|||
isEmojiAction: Bool = false,
|
||||
isEmojiPlus: Bool = false,
|
||||
isDismissAction: Bool = false,
|
||||
accessibilityLabel: String? = nil,
|
||||
work: @escaping () -> Void
|
||||
) {
|
||||
self.icon = icon
|
||||
|
@ -27,6 +29,7 @@ extension ContextMenuVC {
|
|||
self.isEmojiAction = isEmojiAction
|
||||
self.isEmojiPlus = isEmojiPlus
|
||||
self.isDismissAction = isDismissAction
|
||||
self.accessibilityLabel = accessibilityLabel
|
||||
self.work = work
|
||||
}
|
||||
|
||||
|
@ -35,7 +38,8 @@ extension ContextMenuVC {
|
|||
static func reply(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action {
|
||||
return Action(
|
||||
icon: UIImage(named: "ic_reply"),
|
||||
title: "context_menu_reply".localized()
|
||||
title: "context_menu_reply".localized(),
|
||||
accessibilityLabel: "Reply to message"
|
||||
) { delegate?.reply(cellViewModel) }
|
||||
}
|
||||
|
||||
|
@ -56,14 +60,16 @@ extension ContextMenuVC {
|
|||
static func delete(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action {
|
||||
return Action(
|
||||
icon: UIImage(named: "ic_trash"),
|
||||
title: "TXT_DELETE_TITLE".localized()
|
||||
title: "TXT_DELETE_TITLE".localized(),
|
||||
accessibilityLabel: "Delete message"
|
||||
) { delegate?.delete(cellViewModel) }
|
||||
}
|
||||
|
||||
static func save(_ cellViewModel: MessageViewModel, _ delegate: ContextMenuActionDelegate?) -> Action {
|
||||
return Action(
|
||||
icon: UIImage(named: "ic_download"),
|
||||
title: "context_menu_save".localized()
|
||||
title: "context_menu_save".localized(),
|
||||
accessibilityLabel: "Save attachment"
|
||||
) { delegate?.save(cellViewModel) }
|
||||
}
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ extension ContextMenuVC {
|
|||
self.dismiss = dismiss
|
||||
|
||||
super.init(frame: CGRect.zero)
|
||||
|
||||
self.accessibilityLabel = action.accessibilityLabel
|
||||
setUpViewHierarchy()
|
||||
}
|
||||
|
||||
|
|
|
@ -69,6 +69,8 @@ extension ConversationVC:
|
|||
title: "modal_call_permission_request_title".localized(),
|
||||
explanation: "modal_call_permission_request_explanation".localized(),
|
||||
confirmTitle: "vc_settings_title".localized(),
|
||||
confirmAccessibilityLabel: "Settings",
|
||||
cancelAccessibilityLabel: "Cancel",
|
||||
dismissOnConfirm: false // Custom dismissal logic
|
||||
) { [weak self] _ in
|
||||
self?.dismiss(animated: true) {
|
||||
|
@ -135,6 +137,8 @@ extension ConversationVC:
|
|||
range: (message as NSString).range(of: self.viewModel.threadData.displayName)
|
||||
),
|
||||
confirmTitle: "modal_blocked_button_title".localized(),
|
||||
confirmAccessibilityLabel: "Confirm block",
|
||||
cancelAccessibilityLabel: "Cancel block",
|
||||
dismissOnConfirm: false // Custom dismissal logic
|
||||
) { [weak self] _ in
|
||||
self?.viewModel.unblockContact()
|
||||
|
@ -405,7 +409,7 @@ extension ConversationVC:
|
|||
// flags appropriately
|
||||
let threadId: String = self.viewModel.threadData.threadId
|
||||
let oldThreadShouldBeVisible: Bool = (self.viewModel.threadData.threadShouldBeVisible == true)
|
||||
let sentTimestampMs: Int64 = Int64(floor((Date().timeIntervalSince1970 * 1000)))
|
||||
let sentTimestampMs: Int64 = SnodeAPI.currentOffsetTimestampMs()
|
||||
let linkPreviewDraft: LinkPreviewDraft? = snInputView.linkPreviewInfo?.draft
|
||||
let quoteModel: QuotedReplyModel? = snInputView.quoteDraftInfo?.model
|
||||
|
||||
|
@ -530,7 +534,7 @@ extension ConversationVC:
|
|||
// flags appropriately
|
||||
let threadId: String = self.viewModel.threadData.threadId
|
||||
let oldThreadShouldBeVisible: Bool = (self.viewModel.threadData.threadShouldBeVisible == true)
|
||||
let sentTimestampMs: Int64 = Int64(floor((Date().timeIntervalSince1970 * 1000)))
|
||||
let sentTimestampMs: Int64 = SnodeAPI.currentOffsetTimestampMs()
|
||||
|
||||
// If this was a message request then approve it
|
||||
approveMessageRequestIfNeeded(
|
||||
|
@ -636,7 +640,7 @@ extension ConversationVC:
|
|||
threadVariant: threadVariant,
|
||||
threadIsMessageRequest: threadIsMessageRequest,
|
||||
direction: .outgoing,
|
||||
timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000))
|
||||
timestampMs: SnodeAPI.currentOffsetTimestampMs()
|
||||
)
|
||||
|
||||
if needsToStartTypingIndicator {
|
||||
|
@ -854,6 +858,8 @@ extension ConversationVC:
|
|||
range: (message as NSString).range(of: cellViewModel.authorName)
|
||||
),
|
||||
confirmTitle: "modal_download_button_title".localized(),
|
||||
confirmAccessibilityLabel: "Download media",
|
||||
cancelAccessibilityLabel: "Don't download media",
|
||||
dismissOnConfirm: false // Custom dismissal logic
|
||||
) { [weak self] _ in
|
||||
self?.viewModel.trustContact()
|
||||
|
@ -1213,7 +1219,7 @@ extension ConversationVC:
|
|||
guard !threadIsMessageRequest else { return }
|
||||
|
||||
// Perform local rate limiting (don't allow more than 20 reactions within 60 seconds)
|
||||
let sentTimestamp: Int64 = Int64(floor(Date().timeIntervalSince1970 * 1000))
|
||||
let sentTimestamp: Int64 = SnodeAPI.currentOffsetTimestampMs()
|
||||
let recentReactionTimestamps: [Int64] = General.cache.wrappedValue.recentReactionTimestamps
|
||||
|
||||
guard
|
||||
|
@ -2038,7 +2044,7 @@ extension ConversationVC:
|
|||
|
||||
// Create URL
|
||||
let directory: String = OWSTemporaryDirectory()
|
||||
let fileName: String = "\(Int64(floor(Date().timeIntervalSince1970 * 1000))).m4a"
|
||||
let fileName: String = "\(SnodeAPI.currentOffsetTimestampMs()).m4a"
|
||||
let url: URL = URL(fileURLWithPath: directory).appendingPathComponent(fileName)
|
||||
|
||||
// Set up audio session
|
||||
|
@ -2279,7 +2285,7 @@ extension ConversationVC {
|
|||
for: self.viewModel.threadData.threadId,
|
||||
threadVariant: self.viewModel.threadData.threadVariant,
|
||||
isNewThread: false,
|
||||
timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000))
|
||||
timestampMs: SnodeAPI.currentOffsetTimestampMs()
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -196,8 +196,11 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
|
|||
lazy var blockedBanner: InfoBanner = {
|
||||
let result: InfoBanner = InfoBanner(
|
||||
message: self.viewModel.blockedBannerMessage,
|
||||
backgroundColor: .danger
|
||||
backgroundColor: .danger,
|
||||
messageLabelAccessibilityLabel: "Blocked banner text"
|
||||
)
|
||||
result.accessibilityLabel = "Blocked banner"
|
||||
result.isAccessibilityElement = true
|
||||
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(unblock))
|
||||
result.addGestureRecognizer(tapGestureRecognizer)
|
||||
|
||||
|
@ -245,6 +248,8 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
|
|||
|
||||
private lazy var messageRequestAcceptButton: UIButton = {
|
||||
let result: SessionButton = SessionButton(style: .bordered, size: .medium)
|
||||
result.accessibilityLabel = "Accept message request"
|
||||
result.isAccessibilityElement = true
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.setTitle("TXT_DELETE_ACCEPT".localized(), for: .normal)
|
||||
result.addTarget(self, action: #selector(acceptMessageRequest), for: .touchUpInside)
|
||||
|
@ -254,6 +259,8 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
|
|||
|
||||
private lazy var messageRequestDeleteButton: UIButton = {
|
||||
let result: SessionButton = SessionButton(style: .destructive, size: .medium)
|
||||
result.accessibilityLabel = "Decline message request"
|
||||
result.isAccessibilityElement = true
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.setTitle("TXT_DECLINE_TITLE".localized(), for: .normal)
|
||||
result.addTarget(self, action: #selector(deleteMessageRequest), for: .touchUpInside)
|
||||
|
@ -263,6 +270,7 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
|
|||
|
||||
private lazy var messageRequestBlockButton: UIButton = {
|
||||
let result: UIButton = UIButton()
|
||||
result.accessibilityLabel = "Block message request"
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.clipsToBounds = true
|
||||
result.titleLabel?.font = UIFont.boldSystemFont(ofSize: 16)
|
||||
|
@ -1099,7 +1107,7 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
|
|||
profilePictureView.addGestureRecognizer(tapGestureRecognizer)
|
||||
|
||||
let settingsButtonItem: UIBarButtonItem = UIBarButtonItem(customView: profilePictureView)
|
||||
settingsButtonItem.accessibilityLabel = "Settings button"
|
||||
settingsButtonItem.accessibilityLabel = "More options"
|
||||
settingsButtonItem.isAccessibilityElement = true
|
||||
|
||||
if SessionCall.isEnabled && !threadData.threadIsNoteToSelf {
|
||||
|
@ -1109,6 +1117,7 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
|
|||
target: self,
|
||||
action: #selector(startCall)
|
||||
)
|
||||
callButton.accessibilityLabel = "Call button"
|
||||
|
||||
navigationItem.rightBarButtonItems = [settingsButtonItem, callButton]
|
||||
}
|
||||
|
@ -1118,7 +1127,7 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
|
|||
|
||||
default:
|
||||
let rightBarButtonItem: UIBarButtonItem = UIBarButtonItem(image: UIImage(named: "Gear"), style: .plain, target: self, action: #selector(openSettings))
|
||||
rightBarButtonItem.accessibilityLabel = "Settings button"
|
||||
rightBarButtonItem.accessibilityLabel = "More options"
|
||||
rightBarButtonItem.isAccessibilityElement = true
|
||||
|
||||
navigationItem.rightBarButtonItems = [rightBarButtonItem]
|
||||
|
|
|
@ -26,28 +26,28 @@ final class ExpandingAttachmentsButton: UIView, InputViewButtonDelegate {
|
|||
// MARK: UI Components
|
||||
lazy var gifButton: InputViewButton = {
|
||||
let result = InputViewButton(icon: #imageLiteral(resourceName: "actionsheet_gif_black"), delegate: self, hasOpaqueBackground: true)
|
||||
result.accessibilityLabel = "accessibility_gif_button".localized()
|
||||
result.accessibilityLabel = "GIF button"
|
||||
|
||||
return result
|
||||
}()
|
||||
lazy var gifButtonContainer = container(for: gifButton)
|
||||
lazy var documentButton: InputViewButton = {
|
||||
let result = InputViewButton(icon: #imageLiteral(resourceName: "actionsheet_document_black"), delegate: self, hasOpaqueBackground: true)
|
||||
result.accessibilityLabel = "accessibility_document_button".localized()
|
||||
result.accessibilityLabel = "Documents folder"
|
||||
|
||||
return result
|
||||
}()
|
||||
lazy var documentButtonContainer = container(for: documentButton)
|
||||
lazy var libraryButton: InputViewButton = {
|
||||
let result = InputViewButton(icon: #imageLiteral(resourceName: "actionsheet_camera_roll_black"), delegate: self, hasOpaqueBackground: true)
|
||||
result.accessibilityLabel = "accessibility_library_button".localized()
|
||||
result.accessibilityLabel = "Images folder"
|
||||
|
||||
return result
|
||||
}()
|
||||
lazy var libraryButtonContainer = container(for: libraryButton)
|
||||
lazy var cameraButton: InputViewButton = {
|
||||
let result = InputViewButton(icon: #imageLiteral(resourceName: "actionsheet_camera_black"), delegate: self, hasOpaqueBackground: true)
|
||||
result.accessibilityLabel = "accessibility_camera_button".localized()
|
||||
result.accessibilityLabel = "Select camera button"
|
||||
|
||||
return result
|
||||
}()
|
||||
|
|
|
@ -50,12 +50,18 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M
|
|||
// MARK: - UI
|
||||
|
||||
private var bottomStackView: UIStackView?
|
||||
private lazy var attachmentsButton = ExpandingAttachmentsButton(delegate: delegate)
|
||||
private lazy var attachmentsButton: ExpandingAttachmentsButton = {
|
||||
let result = ExpandingAttachmentsButton(delegate: delegate)
|
||||
result.accessibilityLabel = "Attachments button"
|
||||
result.isAccessibilityElement = true
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var voiceMessageButton: InputViewButton = {
|
||||
let result = InputViewButton(icon: #imageLiteral(resourceName: "Microphone"), delegate: self)
|
||||
result.accessibilityLabel = "VOICE_MESSAGE_TOO_SHORT_ALERT_TITLE".localized()
|
||||
result.accessibilityHint = "VOICE_MESSAGE_TOO_SHORT_ALERT_MESSAGE".localized()
|
||||
result.accessibilityLabel = "New voice message"
|
||||
result.isAccessibilityElement = true
|
||||
|
||||
return result
|
||||
}()
|
||||
|
@ -63,7 +69,8 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M
|
|||
private lazy var sendButton: InputViewButton = {
|
||||
let result = InputViewButton(icon: #imageLiteral(resourceName: "ArrowUp"), isSendButton: true, delegate: self)
|
||||
result.isHidden = true
|
||||
result.accessibilityLabel = "ATTACHMENT_APPROVAL_SEND_BUTTON".localized()
|
||||
result.accessibilityLabel = "Send message button"
|
||||
result.isAccessibilityElement = true
|
||||
|
||||
return result
|
||||
}()
|
||||
|
@ -78,6 +85,8 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M
|
|||
|
||||
private lazy var mentionsViewContainer: UIView = {
|
||||
let result: UIView = UIView()
|
||||
result.accessibilityLabel = "Mentions list"
|
||||
result.isAccessibilityElement = true
|
||||
result.alpha = 0
|
||||
|
||||
let backgroundView = UIView()
|
||||
|
@ -107,7 +116,11 @@ final class InputView: UIView, InputViewButtonDelegate, InputTextViewDelegate, M
|
|||
// setUpViewHierarchy() for why these values are the way they are.
|
||||
let adjustment = (InputViewButton.expandedSize - InputViewButton.size) / 2
|
||||
let maxWidth = UIScreen.main.bounds.width - 2 * InputViewButton.expandedSize - 2 * Values.smallSpacing - 2 * (Values.mediumSpacing - adjustment)
|
||||
return InputTextView(delegate: self, maxWidth: maxWidth)
|
||||
let result = InputTextView(delegate: self, maxWidth: maxWidth)
|
||||
result.accessibilityLabel = "Message input box"
|
||||
result.isAccessibilityElement = true
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var disabledInputLabel: UILabel = {
|
||||
|
|
|
@ -24,6 +24,7 @@ final class MentionSelectionView: UIView, UITableViewDataSource, UITableViewDele
|
|||
|
||||
private lazy var tableView: UITableView = {
|
||||
let result: UITableView = UITableView()
|
||||
result.accessibilityLabel = "Contact"
|
||||
result.dataSource = self
|
||||
result.delegate = self
|
||||
result.separatorStyle = .none
|
||||
|
|
|
@ -12,7 +12,7 @@ final class MediaPlaceholderView: UIView {
|
|||
|
||||
init(cellViewModel: MessageViewModel, textColor: ThemeValue) {
|
||||
super.init(frame: CGRect.zero)
|
||||
|
||||
self.accessibilityLabel = "Untrusted attachment message"
|
||||
setUpViewHierarchy(cellViewModel: cellViewModel, textColor: textColor)
|
||||
}
|
||||
|
||||
|
|
|
@ -70,6 +70,8 @@ final class InfoMessageCell: MessageCell {
|
|||
) {
|
||||
guard cellViewModel.variant.isInfoMessage else { return }
|
||||
|
||||
self.accessibilityIdentifier = "Configuration message"
|
||||
self.isAccessibilityElement = true
|
||||
self.viewModel = cellViewModel
|
||||
|
||||
let icon: UIImage? = {
|
||||
|
|
|
@ -142,17 +142,33 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
|
||||
private lazy var reactionContainerView = ReactionContainerView()
|
||||
|
||||
internal lazy var messageStatusContainerView: UIView = {
|
||||
let result = UIView()
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
internal lazy var messageStatusLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.accessibilityLabel = "Message sent status"
|
||||
result.font = .systemFont(ofSize: Values.verySmallFontSize)
|
||||
result.themeTextColor = .messageBubble_deliveryStatus
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
internal lazy var messageStatusImageView: UIImageView = {
|
||||
let result = UIImageView()
|
||||
result.accessibilityLabel = "Message sent status tick"
|
||||
result.contentMode = .scaleAspectFit
|
||||
result.layer.cornerRadius = VisibleMessageCell.messageStatusImageViewSize / 2
|
||||
result.layer.masksToBounds = true
|
||||
result.themeTintColor = .messageBubble_deliveryStatus
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: - Settings
|
||||
|
||||
private static let messageStatusImageViewSize: CGFloat = 16
|
||||
private static let messageStatusImageViewSize: CGFloat = 12
|
||||
private static let authorLabelBottomSpacing: CGFloat = 4
|
||||
private static let groupThreadHSpacing: CGFloat = 12
|
||||
private static let profilePictureSize = Values.verySmallProfilePictureSize
|
||||
|
@ -235,13 +251,22 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
underBubbleStackView.pin(.bottom, to: .bottom, of: self)
|
||||
|
||||
underBubbleStackView.addArrangedSubview(reactionContainerView)
|
||||
underBubbleStackView.addArrangedSubview(messageStatusImageView)
|
||||
underBubbleStackView.addArrangedSubview(messageStatusContainerView)
|
||||
|
||||
messageStatusContainerView.addSubview(messageStatusLabel)
|
||||
messageStatusContainerView.addSubview(messageStatusImageView)
|
||||
|
||||
reactionContainerView.widthAnchor
|
||||
.constraint(lessThanOrEqualTo: underBubbleStackView.widthAnchor)
|
||||
.isActive = true
|
||||
messageStatusImageView.pin(.top, to: .top, of: messageStatusContainerView)
|
||||
messageStatusImageView.pin(.bottom, to: .bottom, of: messageStatusContainerView)
|
||||
messageStatusImageView.pin(.trailing, to: .trailing, of: messageStatusContainerView)
|
||||
messageStatusImageView.set(.width, to: VisibleMessageCell.messageStatusImageViewSize)
|
||||
messageStatusImageView.set(.height, to: VisibleMessageCell.messageStatusImageViewSize)
|
||||
messageStatusLabel.center(.vertical, in: messageStatusContainerView)
|
||||
messageStatusLabel.pin(.leading, to: .leading, of: messageStatusContainerView)
|
||||
messageStatusLabel.pin(.trailing, to: .leading, of: messageStatusImageView, withInset: -2)
|
||||
}
|
||||
|
||||
override func setUpGestureRecognizers() {
|
||||
|
@ -268,7 +293,9 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
lastSearchText: String?
|
||||
) {
|
||||
self.viewModel = cellViewModel
|
||||
|
||||
self.bubbleView.accessibilityIdentifier = "Message Body"
|
||||
self.bubbleView.isAccessibilityElement = true
|
||||
self.bubbleView.accessibilityLabel = cellViewModel.body
|
||||
// We want to add spacing between "clusters" of messages to indicate that time has
|
||||
// passed (even if there wasn't enough time to warrant showing a date header)
|
||||
let shouldAddTopInset: Bool = (
|
||||
|
@ -386,13 +413,15 @@ final class VisibleMessageCell: MessageCell, TappableLabelDelegate {
|
|||
)
|
||||
|
||||
// Message status image view
|
||||
let (image, tintColor) = cellViewModel.state.statusIconInfo(
|
||||
let (image, statusText, tintColor) = cellViewModel.state.statusIconInfo(
|
||||
variant: cellViewModel.variant,
|
||||
hasAtLeastOneReadReceipt: cellViewModel.hasAtLeastOneReadReceipt
|
||||
)
|
||||
messageStatusLabel.text = statusText
|
||||
messageStatusLabel.themeTextColor = tintColor
|
||||
messageStatusImageView.image = image
|
||||
messageStatusImageView.themeTintColor = tintColor
|
||||
messageStatusImageView.isHidden = (
|
||||
messageStatusContainerView.isHidden = (
|
||||
cellViewModel.variant != .standardOutgoing ||
|
||||
cellViewModel.variant == .infoCall ||
|
||||
(
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
#import <QuartzCore/QuartzCore.h>
|
||||
#import <SignalCoreKit/NSDate+OWS.h>
|
||||
#import <SessionUtilitiesKit/NSTimer+Proxying.h>
|
||||
#import <SessionSnodeKit/SessionSnodeKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
|
@ -75,7 +76,7 @@ const CGFloat kDisappearingMessageIconSize = 12.f;
|
|||
return;
|
||||
}
|
||||
|
||||
uint64_t nowTimestamp = [NSDate ows_millisecondTimeStamp];
|
||||
uint64_t nowTimestamp = [SNSnodeAPI currentOffsetTimestampMs];
|
||||
CGFloat secondsLeft
|
||||
= (self.expirationTimestamp > nowTimestamp ? (self.expirationTimestamp - nowTimestamp) / 1000.f : 0.f);
|
||||
CGFloat progress = 0.f;
|
||||
|
|
|
@ -168,7 +168,7 @@ class ThreadDisappearingMessagesViewModel: SessionTableViewModel<ThreadDisappear
|
|||
authorId: getUserHexEncodedPublicKey(db),
|
||||
variant: .infoDisappearingMessagesUpdate,
|
||||
body: config.messageInfoString(with: nil),
|
||||
timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000))
|
||||
timestampMs: SnodeAPI.currentOffsetTimestampMs()
|
||||
)
|
||||
.inserted(db)
|
||||
|
||||
|
|
|
@ -120,7 +120,7 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
|||
NavItem(
|
||||
id: .done,
|
||||
systemItem: .done,
|
||||
accessibilityIdentifier: "Done button"
|
||||
accessibilityIdentifier: "Done"
|
||||
) { [weak self] in
|
||||
self?.setIsEditing(false)
|
||||
|
||||
|
@ -151,7 +151,8 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
|||
NavItem(
|
||||
id: .edit,
|
||||
systemItem: .edit,
|
||||
accessibilityIdentifier: "Edit button"
|
||||
accessibilityIdentifier: "Edit button",
|
||||
accessibilityLabel: "Edit user nickname"
|
||||
) { [weak self] in self?.setIsEditing(true) }
|
||||
]
|
||||
}
|
||||
|
@ -240,6 +241,7 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
|||
"vc_conversation_settings_copy_session_id_button_title".localized()
|
||||
),
|
||||
accessibilityIdentifier: "\(ThreadSettingsViewModel.self).copy_thread_id",
|
||||
accessibilityLabel: "Copy Session ID",
|
||||
onTap: {
|
||||
switch threadVariant {
|
||||
case .contact, .closedGroup:
|
||||
|
@ -275,6 +277,7 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
|||
),
|
||||
title: MediaStrings.allMedia,
|
||||
accessibilityIdentifier: "\(ThreadSettingsViewModel.self).all_media",
|
||||
accessibilityLabel: "All media",
|
||||
onTap: { [weak self] in
|
||||
self?.transitionToScreen(
|
||||
MediaGalleryViewModel.createAllMediaViewController(
|
||||
|
@ -294,6 +297,7 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
|||
),
|
||||
title: "CONVERSATION_SETTINGS_SEARCH".localized(),
|
||||
accessibilityIdentifier: "\(ThreadSettingsViewModel.self).search",
|
||||
accessibilityLabel: "Search",
|
||||
onTap: { [weak self] in
|
||||
self?.didTriggerSearch()
|
||||
}
|
||||
|
@ -344,6 +348,8 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
|||
"DISAPPEARING_MESSAGES_SUBTITLE_OFF".localized()
|
||||
),
|
||||
accessibilityIdentifier: "\(ThreadSettingsViewModel.self).disappearing_messages",
|
||||
accessibilityLabel: "Disappearing messages",
|
||||
leftAccessoryAccessibilityLabel: "Timer icon",
|
||||
onTap: { [weak self] in
|
||||
self?.transitionToScreen(
|
||||
SessionTableViewController(
|
||||
|
@ -365,7 +371,8 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
|||
.withRenderingMode(.alwaysTemplate)
|
||||
),
|
||||
title: "EDIT_GROUP_ACTION".localized(),
|
||||
accessibilityIdentifier: "\(ThreadSettingsViewModel.self).edit_group",
|
||||
accessibilityIdentifier: "Edit group",
|
||||
accessibilityLabel: "Edit group",
|
||||
onTap: { [weak self] in
|
||||
self?.transitionToScreen(EditClosedGroupVC(threadId: threadId))
|
||||
}
|
||||
|
@ -380,7 +387,8 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
|||
.withRenderingMode(.alwaysTemplate)
|
||||
),
|
||||
title: "LEAVE_GROUP_ACTION".localized(),
|
||||
accessibilityIdentifier: "\(ThreadSettingsViewModel.self).leave_group",
|
||||
accessibilityIdentifier: "Leave group",
|
||||
accessibilityLabel: "Leave group",
|
||||
confirmationInfo: ConfirmationModal.Info(
|
||||
title: "CONFIRM_LEAVE_GROUP_TITLE".localized(),
|
||||
explanation: (currentUserIsClosedGroupMember ?
|
||||
|
@ -436,7 +444,8 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
|||
threadViewModel.threadVariant != .closedGroup ||
|
||||
currentUserIsClosedGroupMember
|
||||
),
|
||||
accessibilityIdentifier: "\(ThreadSettingsViewModel.self).notify_for_mentions_only",
|
||||
accessibilityIdentifier: "Mentions only notification setting",
|
||||
accessibilityLabel: "Mentions only",
|
||||
onTap: {
|
||||
let newValue: Bool = !(threadViewModel.threadOnlyNotifyForMentions == true)
|
||||
|
||||
|
@ -469,6 +478,7 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
|||
currentUserIsClosedGroupMember
|
||||
),
|
||||
accessibilityIdentifier: "\(ThreadSettingsViewModel.self).mute",
|
||||
accessibilityLabel: "Mute notifications",
|
||||
onTap: {
|
||||
dependencies.storage.writeAsync { db in
|
||||
let currentValue: TimeInterval? = try SessionThread
|
||||
|
@ -505,6 +515,7 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
|||
.boolValue(threadViewModel.threadIsBlocked == true)
|
||||
),
|
||||
accessibilityIdentifier: "\(ThreadSettingsViewModel.self).block",
|
||||
accessibilityLabel: "Block",
|
||||
confirmationInfo: ConfirmationModal.Info(
|
||||
title: {
|
||||
guard threadViewModel.threadIsBlocked == true else {
|
||||
|
@ -527,6 +538,7 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
|||
"BLOCK_LIST_UNBLOCK_BUTTON".localized() :
|
||||
"BLOCK_LIST_BLOCK_BUTTON".localized()
|
||||
),
|
||||
confirmAccessibilityLabel: "Confirm block",
|
||||
confirmStyle: .danger,
|
||||
cancelStyle: .alert_text
|
||||
),
|
||||
|
@ -610,7 +622,7 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
|||
threadId: thread.id,
|
||||
authorId: userId,
|
||||
variant: .standardOutgoing,
|
||||
timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000)),
|
||||
timestampMs: SnodeAPI.currentOffsetTimestampMs(),
|
||||
expiresInSeconds: try? DisappearingMessagesConfiguration
|
||||
.select(.durationSeconds)
|
||||
.filter(id: userId)
|
||||
|
@ -665,7 +677,10 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
|||
) :
|
||||
nil
|
||||
),
|
||||
accessibilityLabel: oldBlockedState == false ? "User blocked" : "Confirm unblock",
|
||||
accessibilityId: "OK",
|
||||
cancelTitle: "BUTTON_OK".localized(),
|
||||
cancelAccessibilityLabel: "OK",
|
||||
cancelStyle: .alert_text
|
||||
)
|
||||
)
|
||||
|
|
|
@ -22,6 +22,8 @@ final class ConversationTitleView: UIView {
|
|||
|
||||
private lazy var titleLabel: UILabel = {
|
||||
let result: UILabel = UILabel()
|
||||
result.accessibilityIdentifier = "Username"
|
||||
result.isAccessibilityElement = true
|
||||
result.font = .boldSystemFont(ofSize: Values.mediumFontSize)
|
||||
result.themeTextColor = .textPrimary
|
||||
result.lineBreakMode = .byTruncatingTail
|
||||
|
@ -124,6 +126,7 @@ final class ConversationTitleView: UIView {
|
|||
)
|
||||
|
||||
self.titleLabel.text = name
|
||||
self.titleLabel.accessibilityLabel = name
|
||||
self.titleLabel.font = .boldSystemFont(
|
||||
ofSize: (shouldHaveSubtitle ?
|
||||
Values.mediumFontSize :
|
||||
|
|
|
@ -4,10 +4,10 @@ import UIKit
|
|||
import SessionUIKit
|
||||
|
||||
final class InfoBanner: UIView {
|
||||
init(message: String, backgroundColor: ThemeValue) {
|
||||
init(message: String, backgroundColor: ThemeValue, messageLabelAccessibilityLabel: String? = nil) {
|
||||
super.init(frame: CGRect.zero)
|
||||
|
||||
setUpViewHierarchy(message: message, backgroundColor: backgroundColor)
|
||||
setUpViewHierarchy(message: message, backgroundColor: backgroundColor, messageLabelAccessibilityLabel: messageLabelAccessibilityLabel)
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
|
@ -18,10 +18,11 @@ final class InfoBanner: UIView {
|
|||
preconditionFailure("Use init(coder:) instead.")
|
||||
}
|
||||
|
||||
private func setUpViewHierarchy(message: String, backgroundColor: ThemeValue) {
|
||||
private func setUpViewHierarchy(message: String, backgroundColor: ThemeValue, messageLabelAccessibilityLabel: String?) {
|
||||
themeBackgroundColor = backgroundColor
|
||||
|
||||
let label: UILabel = UILabel()
|
||||
label.accessibilityLabel = messageLabelAccessibilityLabel
|
||||
label.font = .boldSystemFont(ofSize: Values.smallFontSize)
|
||||
label.text = message
|
||||
label.themeTextColor = .textPrimary
|
||||
|
|
|
@ -42,6 +42,7 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, SeedRemi
|
|||
|
||||
private lazy var seedReminderView: SeedReminderView = {
|
||||
let result = SeedReminderView(hasContinueButton: true)
|
||||
result.accessibilityLabel = "Recovery phrase reminder"
|
||||
let title = "You're almost finished! 80%"
|
||||
result.subtitle = "view_seed_reminder_subtitle_1".localized()
|
||||
result.setProgress(0.8, animated: false)
|
||||
|
@ -107,6 +108,8 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, SeedRemi
|
|||
result.set(.height, to: HomeVC.newConversationButtonSize)
|
||||
|
||||
let button = UIButton()
|
||||
button.accessibilityLabel = "New conversation button"
|
||||
button.isAccessibilityElement = true
|
||||
button.clipsToBounds = true
|
||||
button.setImage(
|
||||
UIImage(named: "Plus")?
|
||||
|
@ -463,7 +466,9 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, SeedRemi
|
|||
// Profile picture view
|
||||
let profilePictureSize = Values.verySmallProfilePictureSize
|
||||
let profilePictureView = ProfilePictureView()
|
||||
profilePictureView.accessibilityLabel = "Settings button"
|
||||
profilePictureView.accessibilityIdentifier = "User settings"
|
||||
profilePictureView.accessibilityLabel = "User settings"
|
||||
profilePictureView.isAccessibilityElement = true
|
||||
profilePictureView.size = profilePictureSize
|
||||
profilePictureView.update(
|
||||
publicKey: getUserHexEncodedPublicKey(),
|
||||
|
@ -482,7 +487,6 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, SeedRemi
|
|||
|
||||
// Container view
|
||||
let profilePictureViewContainer = UIView()
|
||||
profilePictureViewContainer.accessibilityLabel = "Settings button"
|
||||
profilePictureViewContainer.addSubview(profilePictureView)
|
||||
profilePictureView.autoPinEdgesToSuperviewEdges()
|
||||
profilePictureViewContainer.addSubview(pathStatusView)
|
||||
|
@ -491,7 +495,6 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, SeedRemi
|
|||
|
||||
// Left bar button item
|
||||
let leftBarButtonItem = UIBarButtonItem(customView: profilePictureViewContainer)
|
||||
leftBarButtonItem.accessibilityLabel = "Settings button"
|
||||
leftBarButtonItem.isAccessibilityElement = true
|
||||
navigationItem.leftBarButtonItem = leftBarButtonItem
|
||||
|
||||
|
@ -521,6 +524,8 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, SeedRemi
|
|||
case .messageRequests:
|
||||
let threadViewModel: SessionThreadViewModel = section.elements[indexPath.row]
|
||||
let cell: MessageRequestsCell = tableView.dequeue(type: MessageRequestsCell.self, for: indexPath)
|
||||
cell.accessibilityIdentifier = "Message requests banner"
|
||||
cell.isAccessibilityElement = true
|
||||
cell.update(with: Int(threadViewModel.threadUnreadCount ?? 0))
|
||||
return cell
|
||||
|
||||
|
@ -528,6 +533,8 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, SeedRemi
|
|||
let threadViewModel: SessionThreadViewModel = section.elements[indexPath.row]
|
||||
let cell: FullConversationCell = tableView.dequeue(type: FullConversationCell.self, for: indexPath)
|
||||
cell.update(with: threadViewModel)
|
||||
cell.accessibilityIdentifier = "Conversation list item"
|
||||
cell.accessibilityLabel = threadViewModel.displayName
|
||||
return cell
|
||||
|
||||
default: preconditionFailure("Other sections should have no content")
|
||||
|
|
|
@ -311,6 +311,8 @@ class MessageRequestsViewController: BaseVC, UITableViewDelegate, UITableViewDat
|
|||
case .threads:
|
||||
let threadViewModel: SessionThreadViewModel = section.elements[indexPath.row]
|
||||
let cell: FullConversationCell = tableView.dequeue(type: FullConversationCell.self, for: indexPath)
|
||||
cell.accessibilityIdentifier = "Message request"
|
||||
cell.isAccessibilityElement = true
|
||||
cell.update(with: threadViewModel)
|
||||
return cell
|
||||
|
||||
|
|
|
@ -14,8 +14,21 @@ final class NewConversationVC: BaseVC, ThemedNavigation, UITableViewDelegate, UI
|
|||
|
||||
var navigationBackground: ThemeValue { .newConversation_background }
|
||||
|
||||
private lazy var newDMButton: NewConversationButton = NewConversationButton(icon: #imageLiteral(resourceName: "Message"), title: "vc_create_private_chat_title".localized())
|
||||
private lazy var newGroupButton: NewConversationButton = NewConversationButton(icon: #imageLiteral(resourceName: "Group"), title: "vc_create_closed_group_title".localized())
|
||||
private lazy var newDMButton: NewConversationButton = {
|
||||
let result = NewConversationButton(icon: #imageLiteral(resourceName: "Message"), title: "vc_create_private_chat_title".localized())
|
||||
result.accessibilityIdentifier = "New direct message"
|
||||
result.isAccessibilityElement = true
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var newGroupButton: NewConversationButton = {
|
||||
let result = NewConversationButton(icon: #imageLiteral(resourceName: "Group"), title: "vc_create_closed_group_title".localized())
|
||||
result.accessibilityLabel = "Create group"
|
||||
result.isAccessibilityElement = true
|
||||
|
||||
return result
|
||||
}()
|
||||
private lazy var joinCommunityButton: NewConversationButton = NewConversationButton(icon: #imageLiteral(resourceName: "Globe"), title: "vc_join_public_chat_title".localized(), shouldShowSeparator: false)
|
||||
|
||||
private lazy var buttonStackView: UIStackView = {
|
||||
|
|
|
@ -253,6 +253,7 @@ private final class EnterPublicKeyVC: UIViewController {
|
|||
let result = TextView(placeholder: "vc_enter_public_key_text_field_hint".localized()) { [weak self] text in
|
||||
self?.nextButton.isEnabled = !text.isEmpty
|
||||
}
|
||||
result.accessibilityLabel = "Session id input box"
|
||||
result.autocapitalizationType = .none
|
||||
|
||||
return result
|
||||
|
@ -389,12 +390,15 @@ private final class EnterPublicKeyVC: UIViewController {
|
|||
)
|
||||
result.alpha = (isKeyboardShowing ? 1 : 0)
|
||||
result.isHidden = !isKeyboardShowing
|
||||
|
||||
result.accessibilityLabel = "Next"
|
||||
result.isAccessibilityElement = true
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var nextButton: SessionButton = {
|
||||
let result = SessionButton(style: .bordered, size: .large)
|
||||
result.accessibilityLabel = "Next"
|
||||
result.isAccessibilityElement = true
|
||||
result.setTitle("next".localized(), for: .normal)
|
||||
result.isEnabled = false
|
||||
result.addTarget(self, action: #selector(startNewDMIfPossible), for: .touchUpInside)
|
||||
|
|
|
@ -455,6 +455,7 @@ import SignalUtilitiesKit
|
|||
result.addArrangedSubview(cancelButton)
|
||||
|
||||
let doneButton = createButton(title: CommonStrings.doneButton, action: #selector(donePressed))
|
||||
doneButton.accessibilityLabel = "Done"
|
||||
result.addArrangedSubview(doneButton)
|
||||
|
||||
return result
|
||||
|
|
|
@ -6,6 +6,7 @@ import Foundation
|
|||
import AVFoundation
|
||||
import PromiseKit
|
||||
import CoreServices
|
||||
import SessionMessagingKit
|
||||
|
||||
protocol PhotoCaptureDelegate: AnyObject {
|
||||
func photoCapture(_ photoCapture: PhotoCapture, didFinishProcessingAttachment attachment: SignalAttachment)
|
||||
|
@ -463,6 +464,9 @@ class CaptureOutput {
|
|||
// leaving it enabled causes all audio to be lost on videos longer
|
||||
// than the default length (10s).
|
||||
movieOutput.movieFragmentInterval = CMTime.invalid
|
||||
|
||||
// Ensure the recorded movie can't go over the maximum file server size
|
||||
movieOutput.maxRecordedFileSize = Int64(FileServerAPI.maxFileSize)
|
||||
}
|
||||
|
||||
var photoOutput: AVCaptureOutput? {
|
||||
|
|
|
@ -215,6 +215,7 @@ class SendMediaNavigationController: UINavigationController {
|
|||
private lazy var mediaLibraryViewController: ImagePickerGridController = {
|
||||
let vc = ImagePickerGridController()
|
||||
vc.delegate = self
|
||||
vc.collectionView.accessibilityLabel = "Images"
|
||||
|
||||
return vc
|
||||
}()
|
||||
|
|
|
@ -596,3 +596,7 @@
|
|||
"MESSAGE_STATE_READ" = "Read";
|
||||
"MESSAGE_STATE_SENT" = "Sent";
|
||||
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request";
|
||||
"MESSAGE_DELIVERY_STATUS_SENDING" = "Sending";
|
||||
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
|
||||
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
|
||||
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
|
||||
|
|
|
@ -596,3 +596,7 @@
|
|||
"MESSAGE_STATE_READ" = "Read";
|
||||
"MESSAGE_STATE_SENT" = "Sent";
|
||||
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request";
|
||||
"MESSAGE_DELIVERY_STATUS_SENDING" = "Sending";
|
||||
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
|
||||
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
|
||||
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
|
||||
|
|
|
@ -596,3 +596,7 @@
|
|||
"MESSAGE_STATE_READ" = "Read";
|
||||
"MESSAGE_STATE_SENT" = "Sent";
|
||||
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request";
|
||||
"MESSAGE_DELIVERY_STATUS_SENDING" = "Sending";
|
||||
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
|
||||
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
|
||||
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
|
||||
|
|
|
@ -596,3 +596,7 @@
|
|||
"MESSAGE_STATE_READ" = "خوانده شد";
|
||||
"MESSAGE_STATE_SENT" = "ارسال شد";
|
||||
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request";
|
||||
"MESSAGE_DELIVERY_STATUS_SENDING" = "Sending";
|
||||
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
|
||||
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
|
||||
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
|
||||
|
|
|
@ -596,3 +596,7 @@
|
|||
"MESSAGE_STATE_READ" = "Read";
|
||||
"MESSAGE_STATE_SENT" = "Sent";
|
||||
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request";
|
||||
"MESSAGE_DELIVERY_STATUS_SENDING" = "Sending";
|
||||
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
|
||||
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
|
||||
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
|
||||
|
|
|
@ -596,3 +596,7 @@
|
|||
"MESSAGE_STATE_READ" = "Read";
|
||||
"MESSAGE_STATE_SENT" = "Sent";
|
||||
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request";
|
||||
"MESSAGE_DELIVERY_STATUS_SENDING" = "Sending";
|
||||
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
|
||||
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
|
||||
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
|
||||
|
|
|
@ -596,3 +596,7 @@
|
|||
"MESSAGE_STATE_READ" = "Read";
|
||||
"MESSAGE_STATE_SENT" = "Sent";
|
||||
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request";
|
||||
"MESSAGE_DELIVERY_STATUS_SENDING" = "Sending";
|
||||
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
|
||||
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
|
||||
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
|
||||
|
|
|
@ -596,3 +596,7 @@
|
|||
"MESSAGE_STATE_READ" = "Read";
|
||||
"MESSAGE_STATE_SENT" = "Sent";
|
||||
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request";
|
||||
"MESSAGE_DELIVERY_STATUS_SENDING" = "Sending";
|
||||
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
|
||||
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
|
||||
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
|
||||
|
|
|
@ -596,3 +596,7 @@
|
|||
"MESSAGE_STATE_READ" = "Read";
|
||||
"MESSAGE_STATE_SENT" = "Sent";
|
||||
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request";
|
||||
"MESSAGE_DELIVERY_STATUS_SENDING" = "Sending";
|
||||
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
|
||||
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
|
||||
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
|
||||
|
|
|
@ -596,3 +596,7 @@
|
|||
"MESSAGE_STATE_READ" = "Read";
|
||||
"MESSAGE_STATE_SENT" = "Sent";
|
||||
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request";
|
||||
"MESSAGE_DELIVERY_STATUS_SENDING" = "Sending";
|
||||
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
|
||||
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
|
||||
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
|
||||
|
|
|
@ -596,3 +596,7 @@
|
|||
"MESSAGE_STATE_READ" = "Read";
|
||||
"MESSAGE_STATE_SENT" = "Sent";
|
||||
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request";
|
||||
"MESSAGE_DELIVERY_STATUS_SENDING" = "Sending";
|
||||
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
|
||||
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
|
||||
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
|
||||
|
|
|
@ -596,3 +596,7 @@
|
|||
"MESSAGE_STATE_READ" = "Read";
|
||||
"MESSAGE_STATE_SENT" = "Sent";
|
||||
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request";
|
||||
"MESSAGE_DELIVERY_STATUS_SENDING" = "Sending";
|
||||
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
|
||||
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
|
||||
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
|
||||
|
|
|
@ -596,3 +596,7 @@
|
|||
"MESSAGE_STATE_READ" = "Read";
|
||||
"MESSAGE_STATE_SENT" = "Sent";
|
||||
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request";
|
||||
"MESSAGE_DELIVERY_STATUS_SENDING" = "Sending";
|
||||
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
|
||||
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
|
||||
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
|
||||
|
|
|
@ -596,3 +596,7 @@
|
|||
"MESSAGE_STATE_READ" = "Read";
|
||||
"MESSAGE_STATE_SENT" = "Sent";
|
||||
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request";
|
||||
"MESSAGE_DELIVERY_STATUS_SENDING" = "Sending";
|
||||
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
|
||||
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
|
||||
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
|
||||
|
|
|
@ -596,3 +596,7 @@
|
|||
"MESSAGE_STATE_READ" = "Read";
|
||||
"MESSAGE_STATE_SENT" = "Sent";
|
||||
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request";
|
||||
"MESSAGE_DELIVERY_STATUS_SENDING" = "Sending";
|
||||
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
|
||||
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
|
||||
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
|
||||
|
|
|
@ -596,3 +596,7 @@
|
|||
"MESSAGE_STATE_READ" = "Read";
|
||||
"MESSAGE_STATE_SENT" = "Sent";
|
||||
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request";
|
||||
"MESSAGE_DELIVERY_STATUS_SENDING" = "Sending";
|
||||
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
|
||||
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
|
||||
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
|
||||
|
|
|
@ -596,3 +596,7 @@
|
|||
"MESSAGE_STATE_READ" = "Read";
|
||||
"MESSAGE_STATE_SENT" = "Sent";
|
||||
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request";
|
||||
"MESSAGE_DELIVERY_STATUS_SENDING" = "Sending";
|
||||
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
|
||||
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
|
||||
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
|
||||
|
|
|
@ -596,3 +596,7 @@
|
|||
"MESSAGE_STATE_READ" = "Read";
|
||||
"MESSAGE_STATE_SENT" = "Sent";
|
||||
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request";
|
||||
"MESSAGE_DELIVERY_STATUS_SENDING" = "Sending";
|
||||
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
|
||||
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
|
||||
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
|
||||
|
|
|
@ -596,3 +596,7 @@
|
|||
"MESSAGE_STATE_READ" = "Read";
|
||||
"MESSAGE_STATE_SENT" = "Sent";
|
||||
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request";
|
||||
"MESSAGE_DELIVERY_STATUS_SENDING" = "Sending";
|
||||
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
|
||||
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
|
||||
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
|
||||
|
|
|
@ -596,3 +596,7 @@
|
|||
"MESSAGE_STATE_READ" = "Read";
|
||||
"MESSAGE_STATE_SENT" = "Sent";
|
||||
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request";
|
||||
"MESSAGE_DELIVERY_STATUS_SENDING" = "Sending";
|
||||
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
|
||||
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
|
||||
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
|
||||
|
|
|
@ -596,3 +596,7 @@
|
|||
"MESSAGE_STATE_READ" = "Read";
|
||||
"MESSAGE_STATE_SENT" = "Sent";
|
||||
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request";
|
||||
"MESSAGE_DELIVERY_STATUS_SENDING" = "Sending";
|
||||
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
|
||||
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
|
||||
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
|
||||
|
|
|
@ -596,3 +596,7 @@
|
|||
"MESSAGE_STATE_READ" = "Read";
|
||||
"MESSAGE_STATE_SENT" = "Sent";
|
||||
"MESSAGE_REQUEST_PENDING_APPROVAL_INFO" = "You will be able to send voice messages and attachments once the recipient has approved this message request";
|
||||
"MESSAGE_DELIVERY_STATUS_SENDING" = "Sending";
|
||||
"MESSAGE_DELIVERY_STATUS_SENT" = "Sent";
|
||||
"MESSAGE_DELIVERY_STATUS_READ" = "Read";
|
||||
"MESSAGE_DELIVERY_STATUS_FAILED" = "Failed to send";
|
||||
|
|
|
@ -532,7 +532,7 @@ class NotificationActionHandler {
|
|||
authorId: getUserHexEncodedPublicKey(db),
|
||||
variant: .standardOutgoing,
|
||||
body: replyText,
|
||||
timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000)),
|
||||
timestampMs: SnodeAPI.currentOffsetTimestampMs(),
|
||||
hasMention: Interaction.isUserMentioned(db, threadId: threadId, body: replyText),
|
||||
expiresInSeconds: try? DisappearingMessagesConfiguration
|
||||
.select(.durationSeconds)
|
||||
|
|
|
@ -15,8 +15,9 @@ final class DisplayNameVC: BaseVC {
|
|||
|
||||
private lazy var displayNameTextField: TextField = {
|
||||
let result = TextField(placeholder: "vc_display_name_text_field_hint".localized())
|
||||
result.accessibilityLabel = "Enter display name"
|
||||
result.isAccessibilityElement = true
|
||||
result.themeBorderColor = .textPrimary
|
||||
result.accessibilityLabel = "Display name text field"
|
||||
|
||||
return result
|
||||
}()
|
||||
|
@ -56,6 +57,7 @@ final class DisplayNameVC: BaseVC {
|
|||
|
||||
// Set up register button
|
||||
let registerButton = SessionButton(style: .filled, size: .large)
|
||||
registerButton.accessibilityLabel = "Continue"
|
||||
registerButton.setTitle("continue_2".localized(), for: UIControl.State.normal)
|
||||
registerButton.addTarget(self, action: #selector(register), for: UIControl.Event.touchUpInside)
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ final class LandingVC: BaseVC {
|
|||
|
||||
private lazy var registerButton: SessionButton = {
|
||||
let result = SessionButton(style: .filled, size: .large)
|
||||
result.accessibilityLabel = "Create session ID"
|
||||
result.setTitle("vc_landing_register_button_title".localized(), for: .normal)
|
||||
result.addTarget(self, action: #selector(register), for: .touchUpInside)
|
||||
|
||||
|
@ -63,6 +64,7 @@ final class LandingVC: BaseVC {
|
|||
|
||||
// Link button
|
||||
let linkButton = UIButton()
|
||||
linkButton.accessibilityLabel = "Link a device"
|
||||
linkButton.titleLabel?.font = .boldSystemFont(ofSize: Values.smallFontSize)
|
||||
linkButton.setTitle("vc_landing_link_button_title".localized(), for: .normal)
|
||||
linkButton.setThemeTitleColor(.textPrimary, for: .normal)
|
||||
|
|
|
@ -194,7 +194,7 @@ private final class RecoveryPhraseVC: UIViewController {
|
|||
private lazy var mnemonicTextView: TextView = {
|
||||
let result = TextView(placeholder: "vc_restore_seed_text_field_hint".localized())
|
||||
result.themeBorderColor = .textPrimary
|
||||
result.accessibilityLabel = "Recovery phrase text view"
|
||||
result.accessibilityLabel = "Enter your recovery phrase"
|
||||
|
||||
return result
|
||||
}()
|
||||
|
@ -232,6 +232,7 @@ private final class RecoveryPhraseVC: UIViewController {
|
|||
|
||||
// Continue button
|
||||
let continueButton = SessionButton(style: .filled, size: .large)
|
||||
continueButton.accessibilityLabel = "Link device"
|
||||
continueButton.setTitle("continue_2".localized(), for: UIControl.State.normal)
|
||||
continueButton.addTarget(self, action: #selector(handleContinueButtonTapped), for: UIControl.Event.touchUpInside)
|
||||
|
||||
|
|
|
@ -69,6 +69,7 @@ final class PNModeVC: BaseVC, OptionViewDelegate {
|
|||
|
||||
// Set up register button
|
||||
let registerButton = SessionButton(style: .filled, size: .large)
|
||||
registerButton.accessibilityLabel = "Continue with settings"
|
||||
registerButton.setTitle("continue_2".localized(), for: .normal)
|
||||
registerButton.addTarget(self, action: #selector(register), for: UIControl.Event.touchUpInside)
|
||||
|
||||
|
|
|
@ -17,7 +17,8 @@ final class RegisterVC : BaseVC {
|
|||
let result = UILabel()
|
||||
result.font = Fonts.spaceMono(ofSize: isIPhone5OrSmaller ? Values.mediumFontSize : 20)
|
||||
result.themeTextColor = .textPrimary
|
||||
result.accessibilityLabel = "Session ID label"
|
||||
result.accessibilityLabel = "Session ID"
|
||||
result.isAccessibilityElement = true
|
||||
result.lineBreakMode = .byCharWrapping
|
||||
result.numberOfLines = 0
|
||||
|
||||
|
@ -26,6 +27,8 @@ final class RegisterVC : BaseVC {
|
|||
|
||||
private lazy var copyPublicKeyButton: SessionButton = {
|
||||
let result = SessionButton(style: .bordered, size: .large)
|
||||
result.accessibilityLabel = "Copy"
|
||||
result.isAccessibilityElement = true
|
||||
result.setTitle("copy".localized(), for: .normal)
|
||||
result.addTarget(self, action: #selector(copyPublicKey), for: .touchUpInside)
|
||||
|
||||
|
@ -86,6 +89,7 @@ final class RegisterVC : BaseVC {
|
|||
|
||||
// Set up register button
|
||||
let registerButton = SessionButton(style: .filled, size: .large)
|
||||
registerButton.accessibilityLabel = "Continue"
|
||||
registerButton.setTitle("continue_2".localized(), for: .normal)
|
||||
registerButton.addTarget(self, action: #selector(register), for: UIControl.Event.touchUpInside)
|
||||
|
||||
|
@ -167,6 +171,9 @@ final class RegisterVC : BaseVC {
|
|||
|
||||
private func updatePublicKeyLabel() {
|
||||
let hexEncodedPublicKey = x25519KeyPair.hexEncodedPublicKey
|
||||
publicKeyLabel.accessibilityLabel = hexEncodedPublicKey
|
||||
publicKeyLabel.accessibilityIdentifier = "Session ID generated"
|
||||
publicKeyLabel.isAccessibilityElement = true
|
||||
let characterCount = hexEncodedPublicKey.count
|
||||
var count = 0
|
||||
let limit = 32
|
||||
|
@ -202,6 +209,8 @@ final class RegisterVC : BaseVC {
|
|||
@objc private func copyPublicKey() {
|
||||
UIPasteboard.general.string = x25519KeyPair.hexEncodedPublicKey
|
||||
copyPublicKeyButton.isUserInteractionEnabled = false
|
||||
copyPublicKeyButton.accessibilityLabel = "Copy session id"
|
||||
copyPublicKeyButton.isAccessibilityElement = true
|
||||
UIView.transition(with: copyPublicKeyButton, duration: 0.25, options: .transitionCrossDissolve, animations: {
|
||||
self.copyPublicKeyButton.setTitle("copied".localized(), for: .normal)
|
||||
}, completion: nil)
|
||||
|
|
|
@ -198,7 +198,7 @@ final class RestoreVC: BaseVC {
|
|||
do {
|
||||
let hexEncodedSeed = try Mnemonic.decode(mnemonic: mnemonic)
|
||||
let seed = Data(hex: hexEncodedSeed)
|
||||
let (ed25519KeyPair, x25519KeyPair) = try! Identity.generate(from: seed)
|
||||
let (ed25519KeyPair, x25519KeyPair) = try Identity.generate(from: seed)
|
||||
Onboarding.Flow.recover.preregister(with: seed, ed25519KeyPair: ed25519KeyPair, x25519KeyPair: x25519KeyPair)
|
||||
mnemonicTextView.resignFirstResponder()
|
||||
|
||||
|
|
|
@ -82,6 +82,8 @@ final class SeedReminderView: UIView {
|
|||
|
||||
// Set up button
|
||||
let button = SessionButton(style: .bordered, size: .small)
|
||||
button.accessibilityLabel = "Continue"
|
||||
button.isAccessibilityElement = true
|
||||
button.setTitle("continue_2".localized(), for: UIControl.State.normal)
|
||||
button.set(.width, to: 96)
|
||||
button.addTarget(self, action: #selector(handleContinueButtonTapped), for: UIControl.Event.touchUpInside)
|
||||
|
|
|
@ -46,6 +46,9 @@ final class SeedVC: BaseVC {
|
|||
|
||||
private lazy var mnemonicLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.accessibilityIdentifier = "Recovery Phrase"
|
||||
result.accessibilityLabel = mnemonic
|
||||
result.isAccessibilityElement = true
|
||||
result.font = Fonts.spaceMono(ofSize: Values.mediumFontSize)
|
||||
result.themeTextColor = .primary
|
||||
result.textAlignment = .center
|
||||
|
@ -72,6 +75,8 @@ final class SeedVC: BaseVC {
|
|||
|
||||
// Set up navigation bar buttons
|
||||
let closeButton = UIBarButtonItem(image: #imageLiteral(resourceName: "X"), style: .plain, target: self, action: #selector(close))
|
||||
closeButton.accessibilityLabel = "Navigate up"
|
||||
closeButton.isAccessibilityElement = true
|
||||
closeButton.themeTintColor = .textPrimary
|
||||
navigationItem.leftBarButtonItem = closeButton
|
||||
|
||||
|
|
|
@ -189,11 +189,13 @@ class PrivacySettingsViewModel: SessionTableViewModel<PrivacySettingsViewModel.N
|
|||
title: "PRIVACY_CALLS_TITLE".localized(),
|
||||
subtitle: "PRIVACY_CALLS_DESCRIPTION".localized(),
|
||||
rightAccessory: .toggle(.settingBool(key: .areCallsEnabled)),
|
||||
accessibilityLabel: "Allow voice and video calls",
|
||||
confirmationInfo: ConfirmationModal.Info(
|
||||
title: "PRIVACY_CALLS_WARNING_TITLE".localized(),
|
||||
explanation: "PRIVACY_CALLS_WARNING_DESCRIPTION".localized(),
|
||||
stateToShow: .whenDisabled,
|
||||
confirmTitle: "continue_2".localized(),
|
||||
confirmAccessibilityLabel: "Enable",
|
||||
confirmStyle: .textPrimary,
|
||||
onConfirm: { _ in Permissions.requestMicrophonePermissionIfNeeded() }
|
||||
),
|
||||
|
|
|
@ -124,7 +124,7 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
|
|||
NavItem(
|
||||
id: .done,
|
||||
systemItem: .done,
|
||||
accessibilityIdentifier: "Done button"
|
||||
accessibilityIdentifier: "Done"
|
||||
) { [weak self] in
|
||||
let updatedNickname: String = (self?.editedDisplayName ?? "")
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
@ -214,6 +214,7 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
|
|||
run: { [weak self] button in
|
||||
self?.copySessionId(profile.id, button: button)
|
||||
}
|
||||
|
||||
),
|
||||
SessionCell.Accessory.ThreadInfoStyle.Action(
|
||||
title: "share".localized(),
|
||||
|
@ -393,13 +394,15 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
|
|||
message: nil,
|
||||
preferredStyle: .actionSheet
|
||||
)
|
||||
actionSheet.addAction(UIAlertAction(
|
||||
let action = UIAlertAction(
|
||||
title: "MEDIA_FROM_LIBRARY_BUTTON".localized(),
|
||||
style: .default,
|
||||
handler: { [weak self] _ in
|
||||
self?.showPhotoLibraryForAvatar()
|
||||
}
|
||||
))
|
||||
)
|
||||
action.accessibilityLabel = "Photo library"
|
||||
actionSheet.addAction(action)
|
||||
actionSheet.addAction(UIAlertAction(title: "cancel".localized(), style: .cancel, handler: nil))
|
||||
|
||||
self.transitionToScreen(actionSheet, transitionType: .present)
|
||||
|
@ -494,6 +497,8 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
|
|||
DispatchQueue.main.async {
|
||||
button.isUserInteractionEnabled = false
|
||||
|
||||
|
||||
|
||||
UIView.transition(
|
||||
with: button,
|
||||
duration: 0.25,
|
||||
|
|
|
@ -104,7 +104,6 @@ public final class FullConversationCell: UITableViewCell {
|
|||
let result: UIImageView = UIImageView()
|
||||
result.clipsToBounds = true
|
||||
result.contentMode = .scaleAspectFit
|
||||
result.layer.cornerRadius = (FullConversationCell.statusIndicatorSize / 2)
|
||||
|
||||
return result
|
||||
}()
|
||||
|
|
|
@ -20,6 +20,7 @@ class SessionTableViewController<NavItemId: Equatable, Section: SessionTableSect
|
|||
private var dataStreamJustFailed: Bool = false
|
||||
private var dataChangeCancellable: AnyCancellable?
|
||||
private var disposables: Set<AnyCancellable> = Set()
|
||||
private var onFooterTap: (() -> ())?
|
||||
|
||||
public var viewModelType: AnyObject.Type { return type(of: viewModel) }
|
||||
|
||||
|
@ -45,6 +46,30 @@ class SessionTableViewController<NavItemId: Equatable, Section: SessionTableSect
|
|||
return result
|
||||
}()
|
||||
|
||||
private lazy var fadeView: GradientView = {
|
||||
let result: GradientView = GradientView()
|
||||
result.themeBackgroundGradient = [
|
||||
.value(.backgroundPrimary, alpha: 0), // Want this to take up 20% (~25pt)
|
||||
.backgroundPrimary,
|
||||
.backgroundPrimary,
|
||||
.backgroundPrimary,
|
||||
.backgroundPrimary
|
||||
]
|
||||
result.set(.height, to: Values.footerGradientHeight(window: UIApplication.shared.keyWindow))
|
||||
result.isHidden = true
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var footerButton: SessionButton = {
|
||||
let result: SessionButton = SessionButton(style: .bordered, size: .medium)
|
||||
result.translatesAutoresizingMaskIntoConstraints = false
|
||||
result.addTarget(self, action: #selector(footerButtonTapped), for: .touchUpInside)
|
||||
result.isHidden = true
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
init(viewModel: SessionTableViewModel<NavItemId, Section, SettingItem>) {
|
||||
|
@ -74,6 +99,8 @@ class SessionTableViewController<NavItemId: Equatable, Section: SessionTableSect
|
|||
|
||||
view.themeBackgroundColor = .backgroundPrimary
|
||||
view.addSubview(tableView)
|
||||
view.addSubview(fadeView)
|
||||
view.addSubview(footerButton)
|
||||
|
||||
setupLayout()
|
||||
setupBinding()
|
||||
|
@ -114,6 +141,13 @@ class SessionTableViewController<NavItemId: Equatable, Section: SessionTableSect
|
|||
|
||||
private func setupLayout() {
|
||||
tableView.pin(to: view)
|
||||
|
||||
fadeView.pin(.leading, to: .leading, of: self.view)
|
||||
fadeView.pin(.trailing, to: .trailing, of: self.view)
|
||||
fadeView.pin(.bottom, to: .bottom, of: self.view)
|
||||
|
||||
footerButton.center(.horizontal, in: self.view)
|
||||
footerButton.pin(.bottom, to: .bottom, of: self.view.safeAreaLayoutGuide, withInset: -Values.smallSpacing)
|
||||
}
|
||||
|
||||
// MARK: - Updating
|
||||
|
@ -257,6 +291,33 @@ class SessionTableViewController<NavItemId: Equatable, Section: SessionTableSect
|
|||
}
|
||||
.store(in: &disposables)
|
||||
|
||||
viewModel.footerButtonInfo
|
||||
.receiveOnMain(immediately: true)
|
||||
.sink { [weak self] buttonInfo in
|
||||
if let buttonInfo: SessionButton.Info = buttonInfo {
|
||||
self?.footerButton.setTitle(buttonInfo.title, for: .normal)
|
||||
self?.footerButton.setStyle(buttonInfo.style)
|
||||
self?.footerButton.isEnabled = buttonInfo.isEnabled
|
||||
}
|
||||
|
||||
self?.onFooterTap = buttonInfo?.onTap
|
||||
self?.fadeView.isHidden = (buttonInfo == nil)
|
||||
self?.footerButton.isHidden = (buttonInfo == nil)
|
||||
|
||||
// If we have a footerButton then we want to manually control the contentInset
|
||||
self?.tableView.contentInsetAdjustmentBehavior = (buttonInfo == nil ? .automatic : .never)
|
||||
self?.tableView.contentInset = UIEdgeInsets(
|
||||
top: 0,
|
||||
left: 0,
|
||||
bottom: (buttonInfo == nil ?
|
||||
0 :
|
||||
Values.footerGradientHeight(window: UIApplication.shared.keyWindow)
|
||||
),
|
||||
right: 0
|
||||
)
|
||||
}
|
||||
.store(in: &disposables)
|
||||
|
||||
viewModel.showToast
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] text, color in
|
||||
|
@ -310,6 +371,10 @@ class SessionTableViewController<NavItemId: Equatable, Section: SessionTableSect
|
|||
.store(in: &disposables)
|
||||
}
|
||||
|
||||
@objc private func footerButtonTapped() {
|
||||
onFooterTap?()
|
||||
}
|
||||
|
||||
// MARK: - UITableViewDataSource
|
||||
|
||||
func numberOfSections(in tableView: UITableView) -> Int {
|
||||
|
|
|
@ -43,6 +43,9 @@ class SessionTableViewModel<NavItemId: Equatable, Section: SessionTableSection,
|
|||
preconditionFailure("abstract class - override in subclass")
|
||||
}
|
||||
open var footerView: AnyPublisher<UIView?, Never> { Just(nil).eraseToAnyPublisher() }
|
||||
open var footerButtonInfo: AnyPublisher<SessionButton.Info?, Never> {
|
||||
Just(nil).eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
func updateSettings(_ updatedSettings: [SectionModel]) {
|
||||
preconditionFailure("abstract class - override in subclass")
|
||||
|
|
|
@ -17,6 +17,9 @@ extension SessionCell {
|
|||
let isEnabled: Bool
|
||||
let shouldHaveBackground: Bool
|
||||
let accessibilityIdentifier: String?
|
||||
let accessibilityLabel: String?
|
||||
let leftAccessoryAccessibilityLabel: String?
|
||||
let rightAccessoryAccessibilityLabel: String?
|
||||
let confirmationInfo: ConfirmationModal.Info?
|
||||
let onTap: ((UIView?) -> Void)?
|
||||
|
||||
|
@ -41,6 +44,9 @@ extension SessionCell {
|
|||
isEnabled: Bool = true,
|
||||
shouldHaveBackground: Bool = true,
|
||||
accessibilityIdentifier: String? = nil,
|
||||
accessibilityLabel: String? = nil,
|
||||
leftAccessoryAccessibilityLabel: String? = nil,
|
||||
rightAccessoryAccessibilityLabel: String? = nil,
|
||||
confirmationInfo: ConfirmationModal.Info? = nil,
|
||||
onTap: ((UIView?) -> Void)?
|
||||
) {
|
||||
|
@ -55,6 +61,9 @@ extension SessionCell {
|
|||
self.isEnabled = isEnabled
|
||||
self.shouldHaveBackground = shouldHaveBackground
|
||||
self.accessibilityIdentifier = accessibilityIdentifier
|
||||
self.accessibilityLabel = accessibilityLabel
|
||||
self.leftAccessoryAccessibilityLabel = leftAccessoryAccessibilityLabel
|
||||
self.rightAccessoryAccessibilityLabel = rightAccessoryAccessibilityLabel
|
||||
self.confirmationInfo = confirmationInfo
|
||||
self.onTap = onTap
|
||||
}
|
||||
|
@ -71,6 +80,9 @@ extension SessionCell {
|
|||
isEnabled: Bool = true,
|
||||
shouldHaveBackground: Bool = true,
|
||||
accessibilityIdentifier: String? = nil,
|
||||
accessibilityLabel: String? = nil,
|
||||
leftAccessoryAccessibilityLabel: String? = nil,
|
||||
rightAccessoryAccessibilityLabel: String? = nil,
|
||||
confirmationInfo: ConfirmationModal.Info? = nil,
|
||||
onTap: (() -> Void)? = nil
|
||||
) {
|
||||
|
@ -85,6 +97,9 @@ extension SessionCell {
|
|||
self.isEnabled = isEnabled
|
||||
self.shouldHaveBackground = shouldHaveBackground
|
||||
self.accessibilityIdentifier = accessibilityIdentifier
|
||||
self.accessibilityLabel = accessibilityLabel
|
||||
self.leftAccessoryAccessibilityLabel = leftAccessoryAccessibilityLabel
|
||||
self.rightAccessoryAccessibilityLabel = rightAccessoryAccessibilityLabel
|
||||
self.confirmationInfo = confirmationInfo
|
||||
self.onTap = (onTap != nil ? { _ in onTap?() } : nil)
|
||||
}
|
||||
|
@ -104,6 +119,9 @@ extension SessionCell {
|
|||
isEnabled.hash(into: &hasher)
|
||||
shouldHaveBackground.hash(into: &hasher)
|
||||
accessibilityIdentifier.hash(into: &hasher)
|
||||
accessibilityLabel.hash(into: &hasher)
|
||||
leftAccessoryAccessibilityLabel.hash(into: &hasher)
|
||||
rightAccessoryAccessibilityLabel.hash(into: &hasher)
|
||||
confirmationInfo.hash(into: &hasher)
|
||||
}
|
||||
|
||||
|
@ -118,7 +136,10 @@ extension SessionCell {
|
|||
lhs.extraAction == rhs.extraAction &&
|
||||
lhs.isEnabled == rhs.isEnabled &&
|
||||
lhs.shouldHaveBackground == rhs.shouldHaveBackground &&
|
||||
lhs.accessibilityIdentifier == rhs.accessibilityIdentifier
|
||||
lhs.accessibilityIdentifier == rhs.accessibilityIdentifier &&
|
||||
lhs.accessibilityLabel == rhs.accessibilityLabel &&
|
||||
lhs.leftAccessoryAccessibilityLabel == rhs.leftAccessoryAccessibilityLabel &&
|
||||
lhs.rightAccessoryAccessibilityLabel == rhs.rightAccessoryAccessibilityLabel
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ extension SessionTableViewModel {
|
|||
let style: UIBarButtonItem.Style
|
||||
let systemItem: UIBarButtonItem.SystemItem?
|
||||
let accessibilityIdentifier: String
|
||||
let accessibilityLabel: String?
|
||||
let action: (() -> Void)?
|
||||
|
||||
// MARK: - Initialization
|
||||
|
@ -20,6 +21,7 @@ extension SessionTableViewModel {
|
|||
id: NavItemId,
|
||||
systemItem: UIBarButtonItem.SystemItem?,
|
||||
accessibilityIdentifier: String,
|
||||
accessibilityLabel: String? = nil,
|
||||
action: (() -> Void)? = nil
|
||||
) {
|
||||
self.id = id
|
||||
|
@ -27,6 +29,7 @@ extension SessionTableViewModel {
|
|||
self.style = .plain
|
||||
self.systemItem = systemItem
|
||||
self.accessibilityIdentifier = accessibilityIdentifier
|
||||
self.accessibilityLabel = accessibilityLabel
|
||||
self.action = action
|
||||
}
|
||||
|
||||
|
@ -35,6 +38,7 @@ extension SessionTableViewModel {
|
|||
image: UIImage?,
|
||||
style: UIBarButtonItem.Style,
|
||||
accessibilityIdentifier: String,
|
||||
accessibilityLabel: String? = nil,
|
||||
action: (() -> Void)? = nil
|
||||
) {
|
||||
self.id = id
|
||||
|
@ -42,6 +46,7 @@ extension SessionTableViewModel {
|
|||
self.style = style
|
||||
self.systemItem = nil
|
||||
self.accessibilityIdentifier = accessibilityIdentifier
|
||||
self.accessibilityLabel = accessibilityLabel
|
||||
self.action = action
|
||||
}
|
||||
|
||||
|
@ -54,7 +59,8 @@ extension SessionTableViewModel {
|
|||
style: style,
|
||||
target: nil,
|
||||
action: nil,
|
||||
accessibilityIdentifier: accessibilityIdentifier
|
||||
accessibilityIdentifier: accessibilityIdentifier,
|
||||
accessibilityLabel: accessibilityLabel
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -62,7 +68,8 @@ extension SessionTableViewModel {
|
|||
barButtonSystemItem: systemItem,
|
||||
target: nil,
|
||||
action: nil,
|
||||
accessibilityIdentifier: accessibilityIdentifier
|
||||
accessibilityIdentifier: accessibilityIdentifier,
|
||||
accessibilityLabel: accessibilityLabel
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -48,7 +48,10 @@ final class UserSelectionVC: BaseVC, UITableViewDataSource, UITableViewDelegate
|
|||
|
||||
setNavBarTitle(navBarTitle)
|
||||
|
||||
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(handleDoneButtonTapped))
|
||||
let doneButton = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(handleDoneButtonTapped))
|
||||
doneButton.accessibilityLabel = "Done"
|
||||
navigationItem.rightBarButtonItem = doneButton
|
||||
|
||||
view.addSubview(tableView)
|
||||
tableView.pin(to: view)
|
||||
}
|
||||
|
@ -69,7 +72,8 @@ final class UserSelectionVC: BaseVC, UITableViewDataSource, UITableViewDelegate
|
|||
title: profile.displayName(),
|
||||
rightAccessory: .radio(isSelected: { [weak self] in
|
||||
self?.selectedUsers.contains(profile.id) == true
|
||||
})
|
||||
}),
|
||||
accessibilityIdentifier: "Contact"
|
||||
),
|
||||
style: .edgeToEdge,
|
||||
position: Position.with(indexPath.row, count: users.count)
|
||||
|
|
|
@ -52,6 +52,8 @@ class SessionAvatarCell: UITableViewCell {
|
|||
|
||||
fileprivate let profilePictureView: ProfilePictureView = {
|
||||
let view: ProfilePictureView = ProfilePictureView()
|
||||
view.accessibilityLabel = "Profile picture"
|
||||
view.isAccessibilityElement = true
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.size = Values.largeProfilePictureSize
|
||||
|
||||
|
@ -61,7 +63,7 @@ class SessionAvatarCell: UITableViewCell {
|
|||
fileprivate let displayNameContainer: UIView = {
|
||||
let view: UIView = UIView()
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.accessibilityLabel = "Edit name text field"
|
||||
view.accessibilityLabel = "Username"
|
||||
view.isAccessibilityElement = true
|
||||
|
||||
return view
|
||||
|
@ -69,6 +71,7 @@ class SessionAvatarCell: UITableViewCell {
|
|||
|
||||
private lazy var displayNameLabel: UILabel = {
|
||||
let label: UILabel = UILabel()
|
||||
label.isAccessibilityElement = true
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
label.font = .ows_mediumFont(withSize: Values.veryLargeFontSize)
|
||||
label.themeTextColor = .textPrimary
|
||||
|
@ -83,7 +86,9 @@ class SessionAvatarCell: UITableViewCell {
|
|||
let textField: TextField = TextField(placeholder: "Enter a name", usesDefaultHeight: false)
|
||||
textField.translatesAutoresizingMaskIntoConstraints = false
|
||||
textField.textAlignment = .center
|
||||
textField.accessibilityLabel = "Edit name text field"
|
||||
textField.accessibilityIdentifier = "Nickname"
|
||||
textField.accessibilityLabel = "Nickname"
|
||||
textField.isAccessibilityElement = true
|
||||
textField.alpha = 0
|
||||
|
||||
return textField
|
||||
|
@ -98,6 +103,7 @@ class SessionAvatarCell: UITableViewCell {
|
|||
|
||||
private let descriptionLabel: SRCopyableLabel = {
|
||||
let label: SRCopyableLabel = SRCopyableLabel()
|
||||
label.accessibilityLabel = "Session ID"
|
||||
label.translatesAutoresizingMaskIntoConstraints = false
|
||||
label.themeTextColor = .textPrimary
|
||||
label.textAlignment = .center
|
||||
|
|
|
@ -225,7 +225,8 @@ extension SessionCell {
|
|||
public func update(
|
||||
with accessory: Accessory?,
|
||||
tintColor: ThemeValue,
|
||||
isEnabled: Bool
|
||||
isEnabled: Bool,
|
||||
accessibilityLabel: String?
|
||||
) {
|
||||
guard let accessory: Accessory = accessory else { return }
|
||||
|
||||
|
@ -234,6 +235,7 @@ extension SessionCell {
|
|||
|
||||
switch accessory {
|
||||
case .icon(let image, let iconSize, let customTint, let shouldFill):
|
||||
imageView.accessibilityLabel = accessibilityLabel
|
||||
imageView.image = image
|
||||
imageView.themeTintColor = (customTint ?? tintColor)
|
||||
imageView.contentMode = (shouldFill ? .scaleAspectFill : .scaleAspectFit)
|
||||
|
@ -256,6 +258,7 @@ extension SessionCell {
|
|||
|
||||
case .iconAsync(let iconSize, let customTint, let shouldFill, let setter):
|
||||
setter(imageView)
|
||||
imageView.accessibilityLabel = accessibilityLabel
|
||||
imageView.themeTintColor = (customTint ?? tintColor)
|
||||
imageView.contentMode = (shouldFill ? .scaleAspectFill : .scaleAspectFit)
|
||||
imageView.isHidden = false
|
||||
|
@ -276,6 +279,7 @@ extension SessionCell {
|
|||
imageViewConstraints.forEach { $0.isActive = true }
|
||||
|
||||
case .toggle(let dataSource):
|
||||
toggleSwitch.accessibilityLabel = accessibilityLabel
|
||||
toggleSwitch.isHidden = false
|
||||
toggleSwitch.isEnabled = isEnabled
|
||||
toggleSwitchConstraints.forEach { $0.isActive = true }
|
||||
|
@ -287,6 +291,7 @@ extension SessionCell {
|
|||
}
|
||||
|
||||
case .dropDown(let dataSource):
|
||||
dropDownLabel.accessibilityLabel = accessibilityLabel
|
||||
dropDownLabel.text = dataSource.currentStringValue
|
||||
dropDownStackView.isHidden = false
|
||||
dropDownStackViewConstraints.forEach { $0.isActive = true }
|
||||
|
@ -302,6 +307,7 @@ extension SessionCell {
|
|||
)
|
||||
radioBorderView.layer.cornerRadius = (size.borderSize / 2)
|
||||
|
||||
radioView.accessibilityLabel = accessibilityLabel
|
||||
radioView.alpha = (wasOldSelection ? 0.3 : 1)
|
||||
radioView.isHidden = (!isSelected && !storedSelection)
|
||||
radioView.themeBackgroundColor = (isSelected || wasOldSelection ?
|
||||
|
@ -322,12 +328,14 @@ extension SessionCell {
|
|||
radioBorderViewConstraints.forEach { $0.isActive = true }
|
||||
|
||||
case .highlightingBackgroundLabel(let title):
|
||||
highlightingBackgroundLabel.accessibilityLabel = accessibilityLabel
|
||||
highlightingBackgroundLabel.text = title
|
||||
highlightingBackgroundLabel.themeTextColor = tintColor
|
||||
highlightingBackgroundLabel.isHidden = false
|
||||
highlightingBackgroundLabelConstraints.forEach { $0.isActive = true }
|
||||
|
||||
case .profile(let profileId, let profile):
|
||||
profilePictureView.accessibilityLabel = accessibilityLabel
|
||||
profilePictureView.update(
|
||||
publicKey: profileId,
|
||||
profile: profile,
|
||||
|
@ -338,6 +346,7 @@ extension SessionCell {
|
|||
|
||||
case .customView(let viewGenerator):
|
||||
let generatedView: UIView = viewGenerator()
|
||||
generatedView.accessibilityLabel = accessibilityLabel
|
||||
addSubview(generatedView)
|
||||
|
||||
generatedView.pin(.top, to: .top, of: self)
|
||||
|
|
|
@ -308,6 +308,8 @@ public class SessionCell: UITableViewCell {
|
|||
self.subtitleExtraView = info.subtitleExtraViewGenerator?()
|
||||
self.onExtraActionTap = info.extraAction?.onTap
|
||||
self.accessibilityIdentifier = info.accessibilityIdentifier
|
||||
self.accessibilityLabel = info.accessibilityLabel
|
||||
self.isAccessibilityElement = true
|
||||
|
||||
let leftFitToEdge: Bool = (info.leftAccessory?.shouldFitToEdge == true)
|
||||
let rightFitToEdge: Bool = (!leftFitToEdge && info.rightAccessory?.shouldFitToEdge == true)
|
||||
|
@ -315,12 +317,14 @@ public class SessionCell: UITableViewCell {
|
|||
leftAccessoryView.update(
|
||||
with: info.leftAccessory,
|
||||
tintColor: info.tintColor,
|
||||
isEnabled: info.isEnabled
|
||||
isEnabled: info.isEnabled,
|
||||
accessibilityLabel: info.leftAccessoryAccessibilityLabel
|
||||
)
|
||||
rightAccessoryView.update(
|
||||
with: info.rightAccessory,
|
||||
tintColor: info.tintColor,
|
||||
isEnabled: info.isEnabled
|
||||
isEnabled: info.isEnabled,
|
||||
accessibilityLabel: info.rightAccessoryAccessibilityLabel
|
||||
)
|
||||
rightAccessoryFillConstraint.isActive = rightFitToEdge
|
||||
contentStackView.layoutMargins = UIEdgeInsets(
|
||||
|
|
|
@ -5,6 +5,7 @@ import GRDB
|
|||
import PromiseKit
|
||||
import WebRTC
|
||||
import SessionUtilitiesKit
|
||||
import SessionSnodeKit
|
||||
|
||||
public protocol WebRTCSessionDelegate: AnyObject {
|
||||
var videoCapturer: RTCVideoCapturer { get }
|
||||
|
@ -179,7 +180,7 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
|
|||
uuid: uuid,
|
||||
kind: .offer,
|
||||
sdps: [ sdp.sdp ],
|
||||
sentTimestampMs: UInt64(floor(Date().timeIntervalSince1970 * 1000))
|
||||
sentTimestampMs: UInt64(SnodeAPI.currentOffsetTimestampMs())
|
||||
),
|
||||
interactionId: nil,
|
||||
in: thread
|
||||
|
|
|
@ -1286,7 +1286,7 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
// so we can reverse-engineer an approximate timestamp by extracting it from
|
||||
// the id (this value is unlikely to match exactly though)
|
||||
let fallbackTimestamp: UInt64 = legacyJob.id
|
||||
.map { UInt64($0.prefix("\(Int(Date().timeIntervalSince1970 * 1000))".count)) }
|
||||
.map { UInt64($0.prefix("\(SnodeAPI.currentOffsetTimestampMs())".count)) }
|
||||
.defaulting(to: 0)
|
||||
let legacyIdentifier: String = identifier(
|
||||
for: threadId,
|
||||
|
@ -1657,7 +1657,7 @@ enum _003_YDBToGRDBMigration: Migration {
|
|||
state: .invalid,
|
||||
contentType: "",
|
||||
byteCount: 0,
|
||||
creationTimestamp: Date().timeIntervalSince1970,
|
||||
creationTimestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000),
|
||||
sourceFilename: nil,
|
||||
downloadUrl: nil,
|
||||
localRelativeFilePath: nil,
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import AVFAudio
|
||||
import AVFoundation
|
||||
import GRDB
|
||||
import PromiseKit
|
||||
import SignalCoreKit
|
||||
import SessionUtilitiesKit
|
||||
import AVFAudio
|
||||
import AVFoundation
|
||||
import SessionSnodeKit
|
||||
|
||||
public struct Attachment: Codable, Identifiable, Equatable, Hashable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
|
||||
public static var databaseTableName: String { "attachment" }
|
||||
|
@ -1062,7 +1063,7 @@ extension Attachment {
|
|||
|
||||
// Check the file size
|
||||
SNLog("File size: \(data.count) bytes.")
|
||||
if Double(data.count) > Double(FileServerAPI.maxFileSize) / FileServerAPI.fileSizeORMultiplier {
|
||||
if data.count > FileServerAPI.maxFileSize {
|
||||
failure?(HTTP.Error.maxFileSizeExceeded)
|
||||
return
|
||||
}
|
||||
|
@ -1114,7 +1115,7 @@ extension Attachment {
|
|||
state: .uploaded,
|
||||
creationTimestamp: (
|
||||
updatedAttachment?.creationTimestamp ??
|
||||
Date().timeIntervalSince1970
|
||||
(TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000)
|
||||
),
|
||||
downloadUrl: "\(FileServerAPI.server)/files/\(fileId)"
|
||||
)
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import Foundation
|
||||
import GRDB
|
||||
import SessionUtilitiesKit
|
||||
import SessionSnodeKit
|
||||
|
||||
/// We can rely on the unique constraints within the `Interaction` table to prevent duplicate `VisibleMessage`
|
||||
/// values from being processed, but some control messages don’t have an associated interaction - this table provides
|
||||
|
@ -168,7 +169,10 @@ internal extension ControlMessageProcessRecord {
|
|||
|
||||
self.threadId = threadId
|
||||
self.timestampMs = timestampMs
|
||||
self.serverExpirationTimestamp = (Date().timeIntervalSince1970 + ControlMessageProcessRecord.defaultExpirationSeconds)
|
||||
self.serverExpirationTimestamp = (
|
||||
(TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000) +
|
||||
ControlMessageProcessRecord.defaultExpirationSeconds
|
||||
)
|
||||
}
|
||||
|
||||
/// This method should only be used for records created during migration from the legacy
|
||||
|
@ -179,7 +183,8 @@ internal extension ControlMessageProcessRecord {
|
|||
/// clean out these excessive entries after `defaultExpirationSeconds`)
|
||||
static func generateLegacyProcessRecords(_ db: Database, receivedMessageTimestamps: [Int64]) throws {
|
||||
let defaultExpirationTimestamp: TimeInterval = (
|
||||
Date().timeIntervalSince1970 + ControlMessageProcessRecord.defaultExpirationSeconds
|
||||
(TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000) +
|
||||
ControlMessageProcessRecord.defaultExpirationSeconds
|
||||
)
|
||||
|
||||
try receivedMessageTimestamps.forEach { timestampMs in
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import Foundation
|
||||
import GRDB
|
||||
import SessionUtilitiesKit
|
||||
import SessionSnodeKit
|
||||
|
||||
public struct DisappearingMessagesConfiguration: Codable, Identifiable, Equatable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
|
||||
public static var databaseTableName: String { "disappearingMessagesConfiguration" }
|
||||
|
@ -206,7 +207,7 @@ public class SMKDisappearingMessagesConfiguration: NSObject {
|
|||
authorId: getUserHexEncodedPublicKey(db),
|
||||
variant: .infoDisappearingMessagesUpdate,
|
||||
body: config.messageInfoString(with: nil),
|
||||
timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000))
|
||||
timestampMs: SnodeAPI.currentOffsetTimestampMs()
|
||||
)
|
||||
.inserted(db)
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import Foundation
|
|||
import GRDB
|
||||
import Sodium
|
||||
import SessionUtilitiesKit
|
||||
import SessionSnodeKit
|
||||
|
||||
public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, MutablePersistableRecord, TableRecord, ColumnExpressible {
|
||||
public static var databaseTableName: String { "interaction" }
|
||||
|
@ -298,7 +299,7 @@ public struct Interaction: Codable, Identifiable, Equatable, FetchableRecord, Mu
|
|||
self.timestampMs = timestampMs
|
||||
self.receivedAtTimestampMs = {
|
||||
switch variant {
|
||||
case .standardIncoming, .standardOutgoing: return Int64(Date().timeIntervalSince1970 * 1000)
|
||||
case .standardIncoming, .standardOutgoing: return SnodeAPI.currentOffsetTimestampMs()
|
||||
|
||||
/// For TSInteractions which are not `standardIncoming` and `standardOutgoing` use the `timestampMs` value
|
||||
default: return timestampMs
|
||||
|
@ -458,7 +459,7 @@ public extension Interaction {
|
|||
job: DisappearingMessagesJob.updateNextRunIfNeeded(
|
||||
db,
|
||||
interactionIds: interactionIds,
|
||||
startedAtMs: (Date().timeIntervalSince1970 * 1000)
|
||||
startedAtMs: TimeInterval(SnodeAPI.currentOffsetTimestampMs())
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import PromiseKit
|
|||
import AFNetworking
|
||||
import SignalCoreKit
|
||||
import SessionUtilitiesKit
|
||||
import SessionSnodeKit
|
||||
|
||||
public struct LinkPreview: Codable, Equatable, Hashable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
|
||||
public static var databaseTableName: String { "linkPreview" }
|
||||
|
@ -60,7 +61,7 @@ public struct LinkPreview: Codable, Equatable, Hashable, FetchableRecord, Persis
|
|||
public init(
|
||||
url: String,
|
||||
timestamp: TimeInterval = LinkPreview.timestampFor(
|
||||
sentTimestampMs: (Date().timeIntervalSince1970 * 1000) // Default to now
|
||||
sentTimestampMs: TimeInterval(SnodeAPI.currentOffsetTimestampMs()) // Default to now
|
||||
),
|
||||
variant: Variant = .standard,
|
||||
title: String?,
|
||||
|
|
|
@ -65,16 +65,37 @@ public struct RecipientState: Codable, Equatable, FetchableRecord, PersistableRe
|
|||
}
|
||||
}
|
||||
|
||||
public func statusIconInfo(variant: Interaction.Variant, hasAtLeastOneReadReceipt: Bool) -> (image: UIImage?, themeTintColor: ThemeValue) {
|
||||
guard variant == .standardOutgoing else { return (nil, .textPrimary) }
|
||||
public func statusIconInfo(variant: Interaction.Variant, hasAtLeastOneReadReceipt: Bool) -> (image: UIImage?, text: String?, themeTintColor: ThemeValue) {
|
||||
guard variant == .standardOutgoing else { return (nil, nil, .textPrimary) }
|
||||
|
||||
switch (self, hasAtLeastOneReadReceipt) {
|
||||
case (.sending, _): return (UIImage(systemName: "ellipsis.circle"), .textPrimary)
|
||||
case (.sending, _):
|
||||
return (
|
||||
UIImage(systemName: "ellipsis.circle"),
|
||||
"MESSAGE_DELIVERY_STATUS_SENDING".localized(),
|
||||
.messageBubble_deliveryStatus
|
||||
)
|
||||
|
||||
case (.sent, false), (.skipped, _):
|
||||
return (UIImage(systemName: "checkmark.circle"), .textPrimary)
|
||||
return (
|
||||
UIImage(systemName: "checkmark.circle"),
|
||||
"MESSAGE_DELIVERY_STATUS_SENT".localized(),
|
||||
.messageBubble_deliveryStatus
|
||||
)
|
||||
|
||||
case (.sent, true):
|
||||
return (
|
||||
UIImage(systemName: "eye.fill"),
|
||||
"MESSAGE_DELIVERY_STATUS_READ".localized(),
|
||||
.messageBubble_deliveryStatus
|
||||
)
|
||||
|
||||
case (.sent, true): return (UIImage(systemName: "checkmark.circle.fill"), .textPrimary)
|
||||
case (.failed, _): return (UIImage(systemName: "exclamationmark.circle"), .danger)
|
||||
case (.failed, _):
|
||||
return (
|
||||
UIImage(systemName: "exclamationmark.triangle"),
|
||||
"MESSAGE_DELIVERY_STATUS_FAILED".localized(),
|
||||
.danger
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import Foundation
|
|||
import GRDB
|
||||
import Sodium
|
||||
import SessionUtilitiesKit
|
||||
import SessionSnodeKit
|
||||
|
||||
public struct SessionThread: Codable, Identifiable, Equatable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
|
||||
public static var databaseTableName: String { "thread" }
|
||||
|
@ -104,7 +105,7 @@ public struct SessionThread: Codable, Identifiable, Equatable, FetchableRecord,
|
|||
public init(
|
||||
id: String,
|
||||
variant: Variant,
|
||||
creationDateTimestamp: TimeInterval = Date().timeIntervalSince1970,
|
||||
creationDateTimestamp: TimeInterval = (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000),
|
||||
shouldBeVisible: Bool = false,
|
||||
isPinned: Bool = false,
|
||||
messageDraft: String? = nil,
|
||||
|
|
|
@ -15,13 +15,9 @@ public final class FileServerAPI: NSObject {
|
|||
@objc public static let server = "http://filev2.getsession.org"
|
||||
public static let serverPublicKey = "da21e1d886c6fbaea313f75298bd64aab03a97ce985b46bb2dad9f2089c8ee59"
|
||||
public static let maxFileSize = (10 * 1024 * 1024) // 10 MB
|
||||
/// The file server has a file size limit of `maxFileSize`, which the Service Nodes try to enforce as well. However, the limit applied by the Service Nodes
|
||||
/// is on the **HTTP request** and not the actual file size. Because the file server expects the file data to be base 64 encoded, the size of the HTTP
|
||||
/// request for a given file will be at least `ceil(n / 3) * 4` bytes, where n is the file size in bytes. This is the minimum size because there might also
|
||||
/// be other parameters in the request. On average the multiplier appears to be about 1.5, so when checking whether the file will exceed the file size limit when
|
||||
/// uploading a file we just divide the size of the file by this number. The alternative would be to actually check the size of the HTTP request but that's only
|
||||
/// possible after proof of work has been calculated and the onion request encryption has happened, which takes several seconds.
|
||||
public static let fileSizeORMultiplier: Double = 2
|
||||
|
||||
/// Standard timeout is 10 seconds which is a little too short fir file upload/download with slightly larger files
|
||||
public static let fileTimeout: TimeInterval = 30
|
||||
|
||||
// MARK: - File Storage
|
||||
|
||||
|
@ -77,7 +73,7 @@ public final class FileServerAPI: NSObject {
|
|||
return Promise(error: error)
|
||||
}
|
||||
|
||||
return OnionRequestAPI.sendOnionRequest(urlRequest, to: request.server, with: serverPublicKey)
|
||||
return OnionRequestAPI.sendOnionRequest(urlRequest, to: request.server, with: serverPublicKey, timeout: FileServerAPI.fileTimeout)
|
||||
.map2 { _, response in
|
||||
guard let response: Data = response else { throw HTTP.Error.parsingFailed }
|
||||
|
||||
|
|
|
@ -145,7 +145,7 @@ public enum AttachmentDownloadJob: JobExecutor {
|
|||
_ = try attachment
|
||||
.with(
|
||||
state: .downloaded,
|
||||
creationTimestamp: Date().timeIntervalSince1970,
|
||||
creationTimestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000),
|
||||
localRelativeFilePath: (
|
||||
attachment.localRelativeFilePath ??
|
||||
Attachment.localRelativeFilePath(from: attachment.originalFilePath)
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import Foundation
|
||||
import GRDB
|
||||
import SessionUtilitiesKit
|
||||
import SessionSnodeKit
|
||||
|
||||
public enum DisappearingMessagesJob: JobExecutor {
|
||||
public static let maxFailureCount: Int = -1
|
||||
|
@ -17,7 +18,7 @@ public enum DisappearingMessagesJob: JobExecutor {
|
|||
deferred: @escaping (Job) -> ()
|
||||
) {
|
||||
// The 'backgroundTask' gets captured and cleared within the 'completion' block
|
||||
let timestampNowMs: TimeInterval = ceil(Date().timeIntervalSince1970 * 1000)
|
||||
let timestampNowMs: TimeInterval = TimeInterval(SnodeAPI.currentOffsetTimestampMs())
|
||||
var backgroundTask: OWSBackgroundTask? = OWSBackgroundTask(label: #function)
|
||||
|
||||
let updatedJob: Job? = Storage.shared.write { db in
|
||||
|
@ -59,10 +60,14 @@ public extension DisappearingMessagesJob {
|
|||
|
||||
guard let nextExpirationTimestampMs: Double = nextExpirationTimestampMs else { return nil }
|
||||
|
||||
/// The `expiresStartedAtMs` timestamp is now based on the `SnodeAPI.currentOffsetTimestampMs()` value
|
||||
/// so we need to make sure offset the `nextRunTimestamp` accordingly to ensure it runs at the correct local time
|
||||
let clockOffsetMs: Int64 = SnodeAPI.clockOffsetMs.wrappedValue
|
||||
|
||||
return try? Job
|
||||
.filter(Job.Columns.variant == Job.Variant.disappearingMessages)
|
||||
.fetchOne(db)?
|
||||
.with(nextRunTimestamp: ceil(nextExpirationTimestampMs / 1000))
|
||||
.with(nextRunTimestamp: ceil((nextExpirationTimestampMs - Double(clockOffsetMs)) / 1000))
|
||||
.saved(db)
|
||||
}
|
||||
|
||||
|
|
|
@ -96,7 +96,7 @@ public enum GarbageCollectionJob: JobExecutor {
|
|||
GROUP BY \(interaction[.threadId])
|
||||
) AS interactionInfo ON interactionInfo.\(threadIdLiteral) = \(interaction[.threadId])
|
||||
WHERE (
|
||||
\(interaction[.timestampMs]) < \(timestampNow - approxSixMonthsInSeconds) AND
|
||||
\(interaction[.timestampMs]) < \((timestampNow - approxSixMonthsInSeconds) * 1000) AND
|
||||
interactionInfo.interactionCount >= \(minInteractionsToTrimSql)
|
||||
)
|
||||
)
|
||||
|
|
|
@ -259,7 +259,10 @@ public extension Message {
|
|||
return try processRawReceivedMessage(
|
||||
db,
|
||||
envelope: envelope,
|
||||
serverExpirationTimestamp: (Date().timeIntervalSince1970 + ControlMessageProcessRecord.defaultExpirationSeconds),
|
||||
serverExpirationTimestamp: (
|
||||
(TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000) +
|
||||
ControlMessageProcessRecord.defaultExpirationSeconds
|
||||
),
|
||||
serverHash: serverHash,
|
||||
handleClosedGroupKeyUpdateMessages: true
|
||||
)
|
||||
|
@ -275,7 +278,10 @@ public extension Message {
|
|||
let processedMessage: ProcessedMessage? = try processRawReceivedMessage(
|
||||
db,
|
||||
envelope: envelope,
|
||||
serverExpirationTimestamp: (Date().timeIntervalSince1970 + ControlMessageProcessRecord.defaultExpirationSeconds),
|
||||
serverExpirationTimestamp: (
|
||||
(TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000) +
|
||||
ControlMessageProcessRecord.defaultExpirationSeconds
|
||||
),
|
||||
serverHash: nil,
|
||||
handleClosedGroupKeyUpdateMessages: false
|
||||
)
|
||||
|
@ -407,7 +413,7 @@ public extension Message {
|
|||
|
||||
let count: Int64 = rawReaction.you ? rawReaction.count - 1 : rawReaction.count
|
||||
|
||||
let timestampMs: Int64 = Int64(floor((Date().timeIntervalSince1970 * 1000)))
|
||||
let timestampMs: Int64 = SnodeAPI.currentOffsetTimestampMs()
|
||||
let maxLength: Int = shouldAddSelfReaction ? 4 : 5
|
||||
let desiredReactorIds: [String] = reactors
|
||||
.filter { $0 != blindedUserPublicKey && $0 != userPublicKey } // Remove current user for now, will add back if needed
|
||||
|
|
|
@ -871,6 +871,7 @@ public enum OpenGroupAPI {
|
|||
],
|
||||
body: bytes
|
||||
),
|
||||
timeout: FileServerAPI.fileTimeout,
|
||||
using: dependencies
|
||||
)
|
||||
.decoded(as: FileUploadResponse.self, on: OpenGroupAPI.workQueue, using: dependencies)
|
||||
|
@ -890,6 +891,7 @@ public enum OpenGroupAPI {
|
|||
server: server,
|
||||
endpoint: .roomFileIndividual(roomToken, fileId)
|
||||
),
|
||||
timeout: FileServerAPI.fileTimeout,
|
||||
using: dependencies
|
||||
)
|
||||
.map { responseInfo, maybeData in
|
||||
|
@ -1391,6 +1393,7 @@ public enum OpenGroupAPI {
|
|||
_ db: Database,
|
||||
request: Request<T, Endpoint>,
|
||||
forceBlinded: Bool = false,
|
||||
timeout: TimeInterval = HTTP.timeout,
|
||||
using dependencies: SMKDependencies = SMKDependencies()
|
||||
) -> Promise<(OnionRequestResponseInfoType, Data?)> {
|
||||
let urlRequest: URLRequest
|
||||
|
@ -1415,6 +1418,6 @@ public enum OpenGroupAPI {
|
|||
return Promise(error: OpenGroupAPIError.signingFailed)
|
||||
}
|
||||
|
||||
return dependencies.onionApi.sendOnionRequest(signedRequest, to: request.server, with: publicKey)
|
||||
return dependencies.onionApi.sendOnionRequest(signedRequest, to: request.server, with: publicKey, timeout: timeout)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import Foundation
|
|||
import GRDB
|
||||
import WebRTC
|
||||
import SessionUtilitiesKit
|
||||
import SessionSnodeKit
|
||||
|
||||
extension MessageReceiver {
|
||||
public static func handleCallMessage(_ db: Database, message: CallMessage) throws {
|
||||
|
@ -189,7 +190,7 @@ extension MessageReceiver {
|
|||
body: String(data: messageInfoData, encoding: .utf8),
|
||||
timestampMs: (
|
||||
message.sentTimestamp.map { Int64($0) } ??
|
||||
Int64(floor(Date().timeIntervalSince1970 * 1000))
|
||||
SnodeAPI.currentOffsetTimestampMs()
|
||||
)
|
||||
)
|
||||
.inserted(db)
|
||||
|
@ -235,7 +236,7 @@ extension MessageReceiver {
|
|||
)
|
||||
let timestampMs: Int64 = (
|
||||
message.sentTimestamp.map { Int64($0) } ??
|
||||
Int64(floor(Date().timeIntervalSince1970 * 1000))
|
||||
SnodeAPI.currentOffsetTimestampMs()
|
||||
)
|
||||
|
||||
guard let messageInfoData: Data = try? JSONEncoder().encode(messageInfo) else { return nil }
|
||||
|
|
|
@ -4,6 +4,7 @@ import Foundation
|
|||
import GRDB
|
||||
import Sodium
|
||||
import SessionUtilitiesKit
|
||||
import SessionSnodeKit
|
||||
|
||||
extension MessageReceiver {
|
||||
public static func handleClosedGroupControlMessage(_ db: Database, _ message: ClosedGroupControlMessage) throws {
|
||||
|
@ -135,7 +136,7 @@ extension MessageReceiver {
|
|||
threadId: groupPublicKey,
|
||||
publicKey: Data(encryptionKeyPair.publicKey),
|
||||
secretKey: Data(encryptionKeyPair.secretKey),
|
||||
receivedTimestamp: Date().timeIntervalSince1970
|
||||
receivedTimestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000)
|
||||
).insert(db)
|
||||
|
||||
// Start polling
|
||||
|
@ -196,7 +197,7 @@ extension MessageReceiver {
|
|||
threadId: groupPublicKey,
|
||||
publicKey: proto.publicKey.removingIdPrefixIfNeeded(),
|
||||
secretKey: proto.privateKey,
|
||||
receivedTimestamp: Date().timeIntervalSince1970
|
||||
receivedTimestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000)
|
||||
).insert(db)
|
||||
}
|
||||
catch {
|
||||
|
@ -231,7 +232,7 @@ extension MessageReceiver {
|
|||
.infoMessage(db, sender: sender),
|
||||
timestampMs: (
|
||||
message.sentTimestamp.map { Int64($0) } ??
|
||||
Int64(floor(Date().timeIntervalSince1970 * 1000))
|
||||
SnodeAPI.currentOffsetTimestampMs()
|
||||
)
|
||||
).inserted(db)
|
||||
}
|
||||
|
@ -307,7 +308,7 @@ extension MessageReceiver {
|
|||
.infoMessage(db, sender: sender),
|
||||
timestampMs: (
|
||||
message.sentTimestamp.map { Int64($0) } ??
|
||||
Int64(floor(Date().timeIntervalSince1970 * 1000))
|
||||
SnodeAPI.currentOffsetTimestampMs()
|
||||
)
|
||||
).inserted(db)
|
||||
}
|
||||
|
@ -383,7 +384,7 @@ extension MessageReceiver {
|
|||
.infoMessage(db, sender: sender),
|
||||
timestampMs: (
|
||||
message.sentTimestamp.map { Int64($0) } ??
|
||||
Int64(floor(Date().timeIntervalSince1970 * 1000))
|
||||
SnodeAPI.currentOffsetTimestampMs()
|
||||
)
|
||||
).inserted(db)
|
||||
}
|
||||
|
@ -461,7 +462,7 @@ extension MessageReceiver {
|
|||
.infoMessage(db, sender: sender),
|
||||
timestampMs: (
|
||||
message.sentTimestamp.map { Int64($0) } ??
|
||||
Int64(floor(Date().timeIntervalSince1970 * 1000))
|
||||
SnodeAPI.currentOffsetTimestampMs()
|
||||
)
|
||||
).inserted(db)
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import Foundation
|
||||
import GRDB
|
||||
import SessionSnodeKit
|
||||
|
||||
extension MessageReceiver {
|
||||
internal static func handleDataExtractionNotification(_ db: Database, message: DataExtractionNotification) throws {
|
||||
|
@ -24,7 +25,7 @@ extension MessageReceiver {
|
|||
}(),
|
||||
timestampMs: (
|
||||
message.sentTimestamp.map { Int64($0) } ??
|
||||
Int64(floor(Date().timeIntervalSince1970 * 1000))
|
||||
SnodeAPI.currentOffsetTimestampMs()
|
||||
)
|
||||
).inserted(db)
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import Foundation
|
|||
import GRDB
|
||||
import SignalCoreKit
|
||||
import SessionUtilitiesKit
|
||||
import SessionSnodeKit
|
||||
|
||||
extension MessageReceiver {
|
||||
internal static func handleMessageRequestResponse(
|
||||
|
@ -123,7 +124,7 @@ extension MessageReceiver {
|
|||
variant: .infoMessageRequestAccepted,
|
||||
timestampMs: (
|
||||
message.sentTimestamp.map { Int64($0) } ??
|
||||
Int64(floor(Date().timeIntervalSince1970 * 1000))
|
||||
SnodeAPI.currentOffsetTimestampMs()
|
||||
)
|
||||
).inserted(db)
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import Sodium
|
|||
import Curve25519Kit
|
||||
import PromiseKit
|
||||
import SessionUtilitiesKit
|
||||
import SessionSnodeKit
|
||||
|
||||
extension MessageSender {
|
||||
public static var distributingKeyPairs: Atomic<[String: [ClosedGroupKeyPair]]> = Atomic([:])
|
||||
|
@ -24,7 +25,7 @@ extension MessageSender {
|
|||
let membersAsData = members.map { Data(hex: $0) }
|
||||
let admins = [ userPublicKey ]
|
||||
let adminsAsData = admins.map { Data(hex: $0) }
|
||||
let formationTimestamp: TimeInterval = Date().timeIntervalSince1970
|
||||
let formationTimestamp: TimeInterval = (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000)
|
||||
let thread: SessionThread = try SessionThread
|
||||
.fetchOrCreate(db, id: groupPublicKey, variant: .closedGroup)
|
||||
try ClosedGroup(
|
||||
|
@ -91,7 +92,7 @@ extension MessageSender {
|
|||
threadId: groupPublicKey,
|
||||
publicKey: encryptionKeyPair.publicKey,
|
||||
secretKey: encryptionKeyPair.privateKey,
|
||||
receivedTimestamp: Date().timeIntervalSince1970
|
||||
receivedTimestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000)
|
||||
).insert(db)
|
||||
|
||||
// Notify the PN server
|
||||
|
@ -110,7 +111,7 @@ extension MessageSender {
|
|||
threadId: thread.id,
|
||||
authorId: userPublicKey,
|
||||
variant: .infoClosedGroupCreated,
|
||||
timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000))
|
||||
timestampMs: SnodeAPI.currentOffsetTimestampMs()
|
||||
).inserted(db)
|
||||
|
||||
// Start polling
|
||||
|
@ -142,7 +143,7 @@ extension MessageSender {
|
|||
threadId: closedGroup.threadId,
|
||||
publicKey: legacyNewKeyPair.publicKey,
|
||||
secretKey: legacyNewKeyPair.privateKey,
|
||||
receivedTimestamp: Date().timeIntervalSince1970
|
||||
receivedTimestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000)
|
||||
)
|
||||
|
||||
// Distribute it
|
||||
|
@ -230,7 +231,7 @@ extension MessageSender {
|
|||
body: ClosedGroupControlMessage.Kind
|
||||
.nameChange(name: name)
|
||||
.infoMessage(db, sender: userPublicKey),
|
||||
timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000))
|
||||
timestampMs: SnodeAPI.currentOffsetTimestampMs()
|
||||
).inserted(db)
|
||||
|
||||
guard let interactionId: Int64 = interaction.id else { throw StorageError.objectNotSaved }
|
||||
|
@ -330,7 +331,7 @@ extension MessageSender {
|
|||
body: ClosedGroupControlMessage.Kind
|
||||
.membersAdded(members: addedMembers.map { Data(hex: $0) })
|
||||
.infoMessage(db, sender: userPublicKey),
|
||||
timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000))
|
||||
timestampMs: SnodeAPI.currentOffsetTimestampMs()
|
||||
).inserted(db)
|
||||
|
||||
guard let interactionId: Int64 = interaction.id else { throw StorageError.objectNotSaved }
|
||||
|
@ -431,7 +432,7 @@ extension MessageSender {
|
|||
body: ClosedGroupControlMessage.Kind
|
||||
.membersRemoved(members: removedMembers.map { Data(hex: $0) })
|
||||
.infoMessage(db, sender: userPublicKey),
|
||||
timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000))
|
||||
timestampMs: SnodeAPI.currentOffsetTimestampMs()
|
||||
).inserted(db)
|
||||
|
||||
guard let newInteractionId: Int64 = interaction.id else { throw StorageError.objectNotSaved }
|
||||
|
@ -496,7 +497,7 @@ extension MessageSender {
|
|||
body: ClosedGroupControlMessage.Kind
|
||||
.memberLeft
|
||||
.infoMessage(db, sender: userPublicKey),
|
||||
timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000))
|
||||
timestampMs: SnodeAPI.currentOffsetTimestampMs()
|
||||
).inserted(db)
|
||||
|
||||
guard let interactionId: Int64 = interaction.id else {
|
||||
|
|
|
@ -5,6 +5,7 @@ import GRDB
|
|||
import Sodium
|
||||
import SignalCoreKit
|
||||
import SessionUtilitiesKit
|
||||
import SessionSnodeKit
|
||||
|
||||
public enum MessageReceiver {
|
||||
private static var lastEncryptionKeyPairRequest: [String: Date] = [:]
|
||||
|
@ -144,7 +145,7 @@ public enum MessageReceiver {
|
|||
message.sender = sender
|
||||
message.recipient = userPublicKey
|
||||
message.sentTimestamp = envelope.timestamp
|
||||
message.receivedTimestamp = UInt64((Date().timeIntervalSince1970) * 1000)
|
||||
message.receivedTimestamp = UInt64(SnodeAPI.currentOffsetTimestampMs())
|
||||
message.groupPublicKey = groupPublicKey
|
||||
message.openGroupServerMessageId = openGroupMessageServerId.map { UInt64($0) }
|
||||
|
||||
|
@ -318,7 +319,7 @@ public enum MessageReceiver {
|
|||
var updatedProfile: Profile = profile
|
||||
|
||||
// Name
|
||||
if let name = name, name != profile.name {
|
||||
if let name = name, !name.isEmpty, name != profile.name {
|
||||
let shouldUpdate: Bool
|
||||
if isCurrentUser {
|
||||
shouldUpdate = given(UserDefaults.standard[.lastDisplayNameUpdate]) {
|
||||
|
|
|
@ -66,8 +66,7 @@ public final class MessageSender {
|
|||
) throws -> Promise<Void> {
|
||||
let (promise, seal) = Promise<Void>.pending()
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
let isMainAppActive: Bool = (UserDefaults.sharedLokiProject?[.isMainAppActive]).defaulting(to: false)
|
||||
let messageSendTimestamp: Int64 = Int64(floor(Date().timeIntervalSince1970 * 1000))
|
||||
let messageSendTimestamp: Int64 = SnodeAPI.currentOffsetTimestampMs()
|
||||
|
||||
// Set the timestamp, sender and recipient
|
||||
message.sentTimestamp = (
|
||||
|
@ -202,7 +201,7 @@ public final class MessageSender {
|
|||
recipient: message.recipient!,
|
||||
data: base64EncodedData,
|
||||
ttl: message.ttl,
|
||||
timestampMs: UInt64(messageSendTimestamp + SnodeAPI.clockOffset.wrappedValue)
|
||||
timestampMs: UInt64(messageSendTimestamp)
|
||||
)
|
||||
|
||||
SnodeAPI
|
||||
|
@ -261,6 +260,8 @@ public final class MessageSender {
|
|||
behaviour: .runOnce,
|
||||
details: NotifyPushServerJob.Details(message: snodeMessage)
|
||||
)
|
||||
let isMainAppActive: Bool = (UserDefaults.sharedLokiProject?[.isMainAppActive])
|
||||
.defaulting(to: false)
|
||||
|
||||
if isMainAppActive {
|
||||
JobRunner.add(db, job: job)
|
||||
|
@ -322,7 +323,7 @@ public final class MessageSender {
|
|||
|
||||
// Set the timestamp, sender and recipient
|
||||
if message.sentTimestamp == nil { // Visible messages will already have their sent timestamp set
|
||||
message.sentTimestamp = UInt64(floor(Date().timeIntervalSince1970 * 1000))
|
||||
message.sentTimestamp = UInt64(SnodeAPI.currentOffsetTimestampMs())
|
||||
}
|
||||
|
||||
switch destination {
|
||||
|
@ -472,7 +473,7 @@ public final class MessageSender {
|
|||
|
||||
// Set the timestamp, sender and recipient
|
||||
if message.sentTimestamp == nil { // Visible messages will already have their sent timestamp set
|
||||
message.sentTimestamp = UInt64(floor(Date().timeIntervalSince1970 * 1000))
|
||||
message.sentTimestamp = UInt64(SnodeAPI.currentOffsetTimestampMs())
|
||||
}
|
||||
|
||||
message.sender = userPublicKey
|
||||
|
@ -617,7 +618,7 @@ public final class MessageSender {
|
|||
job: DisappearingMessagesJob.updateNextRunIfNeeded(
|
||||
db,
|
||||
interaction: interaction,
|
||||
startedAtMs: (Date().timeIntervalSince1970 * 1000)
|
||||
startedAtMs: TimeInterval(SnodeAPI.currentOffsetTimestampMs())
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -636,7 +637,10 @@ public final class MessageSender {
|
|||
}
|
||||
}(),
|
||||
message: message,
|
||||
serverExpirationTimestamp: (Date().timeIntervalSince1970 + ControlMessageProcessRecord.defaultExpirationSeconds)
|
||||
serverExpirationTimestamp: (
|
||||
(TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000) +
|
||||
ControlMessageProcessRecord.defaultExpirationSeconds
|
||||
)
|
||||
)?.insert(db)
|
||||
|
||||
// Sync the message if:
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import Foundation
|
||||
import GRDB
|
||||
import SessionUtilitiesKit
|
||||
import SessionSnodeKit
|
||||
|
||||
public class TypingIndicators {
|
||||
// MARK: - Direction
|
||||
|
@ -41,7 +42,7 @@ public class TypingIndicators {
|
|||
|
||||
self.threadId = threadId
|
||||
self.direction = direction
|
||||
self.timestampMs = (timestampMs ?? Int64(floor(Date().timeIntervalSince1970 * 1000)))
|
||||
self.timestampMs = (timestampMs ?? SnodeAPI.currentOffsetTimestampMs())
|
||||
}
|
||||
|
||||
fileprivate func start(_ db: Database) {
|
||||
|
|
|
@ -196,7 +196,7 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
|
|||
authorId: getUserHexEncodedPublicKey(db),
|
||||
variant: .standardOutgoing,
|
||||
body: body,
|
||||
timestampMs: Int64(floor(Date().timeIntervalSince1970 * 1000)),
|
||||
timestampMs: SnodeAPI.currentOffsetTimestampMs(),
|
||||
hasMention: Interaction.isUserMentioned(db, threadId: threadId, body: body),
|
||||
expiresInSeconds: try? DisappearingMessagesConfiguration
|
||||
.select(.durationSeconds)
|
||||
|
|
|
@ -93,7 +93,7 @@ public extension SnodeReceivedMessageInfo {
|
|||
return try SnodeReceivedMessageInfo
|
||||
.select(Column.rowID)
|
||||
.filter(SnodeReceivedMessageInfo.Columns.key == key(for: snode, publicKey: publicKey, namespace: namespace))
|
||||
.filter(SnodeReceivedMessageInfo.Columns.expirationDateMs <= (Date().timeIntervalSince1970 * 1000))
|
||||
.filter(SnodeReceivedMessageInfo.Columns.expirationDateMs <= SnodeAPI.currentOffsetTimestampMs())
|
||||
.asRequest(of: Int64.self)
|
||||
.fetchAll(db)
|
||||
}
|
||||
|
@ -122,7 +122,7 @@ public extension SnodeReceivedMessageInfo {
|
|||
SnodeReceivedMessageInfo.Columns.wasDeletedOrInvalid == false
|
||||
)
|
||||
.filter(SnodeReceivedMessageInfo.Columns.key == key(for: snode, publicKey: publicKey, namespace: namespace))
|
||||
.filter(SnodeReceivedMessageInfo.Columns.expirationDateMs > (Date().timeIntervalSince1970 * 1000))
|
||||
.filter(SnodeReceivedMessageInfo.Columns.expirationDateMs > SnodeAPI.currentOffsetTimestampMs())
|
||||
.order(SnodeReceivedMessageInfo.Columns.id.desc)
|
||||
.fetchOne(db)
|
||||
|
||||
|
|
|
@ -7,13 +7,17 @@ import PromiseKit
|
|||
import SessionUtilitiesKit
|
||||
|
||||
public protocol OnionRequestAPIType {
|
||||
static func sendOnionRequest(to snode: Snode, invoking method: SnodeAPIEndpoint, with parameters: JSON, associatedWith publicKey: String?) -> Promise<Data>
|
||||
static func sendOnionRequest(_ request: URLRequest, to server: String, using version: OnionRequestAPIVersion, with x25519PublicKey: String) -> Promise<(OnionRequestResponseInfoType, Data?)>
|
||||
static func sendOnionRequest(to snode: Snode, invoking method: SnodeAPIEndpoint, with parameters: JSON, associatedWith publicKey: String?, timeout: TimeInterval) -> Promise<Data>
|
||||
static func sendOnionRequest(_ request: URLRequest, to server: String, using version: OnionRequestAPIVersion, with x25519PublicKey: String, timeout: TimeInterval) -> Promise<(OnionRequestResponseInfoType, Data?)>
|
||||
}
|
||||
|
||||
public extension OnionRequestAPIType {
|
||||
static func sendOnionRequest(_ request: URLRequest, to server: String, with x25519PublicKey: String) -> Promise<(OnionRequestResponseInfoType, Data?)> {
|
||||
sendOnionRequest(request, to: server, using: .v4, with: x25519PublicKey)
|
||||
static func sendOnionRequest(to snode: Snode, invoking method: SnodeAPIEndpoint, with parameters: JSON, associatedWith publicKey: String?) -> Promise<Data> {
|
||||
sendOnionRequest(to: snode, invoking: method, with: parameters, associatedWith: publicKey, timeout: HTTP.timeout)
|
||||
}
|
||||
|
||||
static func sendOnionRequest(_ request: URLRequest, to server: String, with x25519PublicKey: String, timeout: TimeInterval = HTTP.timeout) -> Promise<(OnionRequestResponseInfoType, Data?)> {
|
||||
sendOnionRequest(request, to: server, using: .v4, with: x25519PublicKey, timeout: timeout)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -369,7 +373,7 @@ public enum OnionRequestAPI: OnionRequestAPIType {
|
|||
// MARK: - Public API
|
||||
|
||||
/// Sends an onion request to `snode`. Builds new paths as needed.
|
||||
public static func sendOnionRequest(to snode: Snode, invoking method: SnodeAPIEndpoint, with parameters: JSON, associatedWith publicKey: String? = nil) -> Promise<Data> {
|
||||
public static func sendOnionRequest(to snode: Snode, invoking method: SnodeAPIEndpoint, with parameters: JSON, associatedWith publicKey: String? = nil, timeout: TimeInterval = HTTP.timeout) -> Promise<Data> {
|
||||
let payloadJson: JSON = [ "method" : method.rawValue, "params" : parameters ]
|
||||
|
||||
guard let payload: Data = try? JSONSerialization.data(withJSONObject: payloadJson, options: []) else {
|
||||
|
@ -377,7 +381,7 @@ public enum OnionRequestAPI: OnionRequestAPIType {
|
|||
}
|
||||
|
||||
/// **Note:** Currently the service nodes only support V3 Onion Requests
|
||||
return sendOnionRequest(with: payload, to: OnionRequestAPIDestination.snode(snode), version: .v3)
|
||||
return sendOnionRequest(with: payload, to: OnionRequestAPIDestination.snode(snode), version: .v3, timeout: timeout)
|
||||
.map { _, maybeData in
|
||||
guard let data: Data = maybeData else { throw HTTP.Error.invalidResponse }
|
||||
|
||||
|
@ -393,7 +397,7 @@ public enum OnionRequestAPI: OnionRequestAPIType {
|
|||
}
|
||||
|
||||
/// Sends an onion request to `server`. Builds new paths as needed.
|
||||
public static func sendOnionRequest(_ request: URLRequest, to server: String, using version: OnionRequestAPIVersion = .v4, with x25519PublicKey: String) -> Promise<(OnionRequestResponseInfoType, Data?)> {
|
||||
public static func sendOnionRequest(_ request: URLRequest, to server: String, using version: OnionRequestAPIVersion = .v4, with x25519PublicKey: String, timeout: TimeInterval = HTTP.timeout) -> Promise<(OnionRequestResponseInfoType, Data?)> {
|
||||
guard let url = request.url, let host = request.url?.host else {
|
||||
return Promise(error: OnionRequestAPIError.invalidURL)
|
||||
}
|
||||
|
@ -412,14 +416,14 @@ public enum OnionRequestAPI: OnionRequestAPIType {
|
|||
scheme: scheme,
|
||||
port: port
|
||||
)
|
||||
let promise = sendOnionRequest(with: payload, to: destination, version: version)
|
||||
let promise = sendOnionRequest(with: payload, to: destination, version: version, timeout: timeout)
|
||||
promise.catch2 { error in
|
||||
SNLog("Couldn't reach server: \(url) due to error: \(error).")
|
||||
}
|
||||
return promise
|
||||
}
|
||||
|
||||
public static func sendOnionRequest(with payload: Data, to destination: OnionRequestAPIDestination, version: OnionRequestAPIVersion) -> Promise<(OnionRequestResponseInfoType, Data?)> {
|
||||
public static func sendOnionRequest(with payload: Data, to destination: OnionRequestAPIDestination, version: OnionRequestAPIVersion, timeout: TimeInterval = HTTP.timeout) -> Promise<(OnionRequestResponseInfoType, Data?)> {
|
||||
let (promise, seal) = Promise<(OnionRequestResponseInfoType, Data?)>.pending()
|
||||
var guardSnode: Snode?
|
||||
|
||||
|
@ -444,7 +448,7 @@ public enum OnionRequestAPI: OnionRequestAPIType {
|
|||
}
|
||||
let destinationSymmetricKey = intermediate.destinationSymmetricKey
|
||||
|
||||
HTTP.execute(.post, url, body: body)
|
||||
HTTP.execute(.post, url, body: body, timeout: timeout)
|
||||
.done2 { responseData in
|
||||
handleResponse(
|
||||
responseData: responseData,
|
||||
|
@ -672,7 +676,7 @@ public enum OnionRequestAPI: OnionRequestAPIType {
|
|||
|
||||
if let timestamp = body["t"] as? Int64 {
|
||||
let offset = timestamp - Int64(floor(Date().timeIntervalSince1970 * 1000))
|
||||
SnodeAPI.clockOffset.mutate { $0 = offset }
|
||||
SnodeAPI.clockOffsetMs.mutate { $0 = offset }
|
||||
}
|
||||
|
||||
guard 200...299 ~= statusCode else {
|
||||
|
|
|
@ -19,10 +19,16 @@ public final class SnodeAPI {
|
|||
internal static var snodePool: Atomic<Set<Snode>> = Atomic([])
|
||||
|
||||
/// The offset between the user's clock and the Service Node's clock. Used in cases where the
|
||||
/// user's clock is incorrect.
|
||||
///
|
||||
/// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions.
|
||||
public static var clockOffset: Atomic<Int64> = Atomic(0)
|
||||
/// user's clock is incorrect
|
||||
public static var clockOffsetMs: Atomic<Int64> = Atomic(0)
|
||||
|
||||
public static func currentOffsetTimestampMs() -> Int64 {
|
||||
return (
|
||||
Int64(floor(Date().timeIntervalSince1970 * 1000)) +
|
||||
SnodeAPI.clockOffsetMs.wrappedValue
|
||||
)
|
||||
}
|
||||
|
||||
/// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions.
|
||||
public static var swarmCache: Atomic<[String: Set<Snode>]> = Atomic([:])
|
||||
|
||||
|
@ -546,7 +552,7 @@ public final class SnodeAPI {
|
|||
let lastHash = SnodeReceivedMessageInfo.fetchLastNotExpired(for: snode, namespace: namespace, associatedWith: publicKey)?.hash ?? ""
|
||||
|
||||
// Construct signature
|
||||
let timestamp = UInt64(Int64(floor(Date().timeIntervalSince1970 * 1000)) + SnodeAPI.clockOffset.wrappedValue)
|
||||
let timestamp = UInt64(SnodeAPI.currentOffsetTimestampMs())
|
||||
let ed25519PublicKey = userED25519KeyPair.publicKey.toHexString()
|
||||
let namespaceVerificationString = (namespace == defaultNamespace ? "" : String(namespace))
|
||||
|
||||
|
@ -647,7 +653,7 @@ public final class SnodeAPI {
|
|||
}
|
||||
|
||||
// Construct signature
|
||||
let timestamp = UInt64(Int64(floor(Date().timeIntervalSince1970 * 1000)) + SnodeAPI.clockOffset.wrappedValue)
|
||||
let timestamp = UInt64(SnodeAPI.currentOffsetTimestampMs())
|
||||
let ed25519PublicKey = userED25519KeyPair.publicKey.toHexString()
|
||||
|
||||
guard
|
||||
|
@ -1102,3 +1108,11 @@ public final class SnodeAPI {
|
|||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@objc(SNSnodeAPI)
|
||||
public final class SNSnodeAPI: NSObject {
|
||||
@objc(currentOffsetTimestampMs)
|
||||
public static func currentOffsetTimestampMs() -> UInt64 {
|
||||
return UInt64(SnodeAPI.currentOffsetTimestampMs())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -228,7 +228,7 @@ class ThreadSettingsViewModelSpec: QuickSpec {
|
|||
ParentType.NavItem(
|
||||
id: .cancel,
|
||||
systemItem: .cancel,
|
||||
accessibilityIdentifier: "Cancel button"
|
||||
accessibilityIdentifier: "Cancel"
|
||||
)
|
||||
]))
|
||||
expect(viewModel.rightNavItems.firstValue())
|
||||
|
@ -236,7 +236,7 @@ class ThreadSettingsViewModelSpec: QuickSpec {
|
|||
ParentType.NavItem(
|
||||
id: .done,
|
||||
systemItem: .done,
|
||||
accessibilityIdentifier: "Done button"
|
||||
accessibilityIdentifier: "Done"
|
||||
)
|
||||
]))
|
||||
}
|
||||
|
@ -369,7 +369,7 @@ class ThreadSettingsViewModelSpec: QuickSpec {
|
|||
ParentType.NavItem(
|
||||
id: .done,
|
||||
systemItem: .done,
|
||||
accessibilityIdentifier: "Done button"
|
||||
accessibilityIdentifier: "Done"
|
||||
)
|
||||
]))
|
||||
}
|
||||
|
|
|
@ -21,10 +21,14 @@ public class ConfirmationModal: Modal {
|
|||
let title: String
|
||||
let explanation: String?
|
||||
let attributedExplanation: NSAttributedString?
|
||||
let accessibilityLabel: String?
|
||||
let accessibilityId: String?
|
||||
public let stateToShow: State
|
||||
let confirmTitle: String?
|
||||
let confirmAccessibilityLabel: String?
|
||||
let confirmStyle: ThemeValue
|
||||
let cancelTitle: String
|
||||
let cancelAccessibilityLabel: String?
|
||||
let cancelStyle: ThemeValue
|
||||
let dismissOnConfirm: Bool
|
||||
let onConfirm: ((UIViewController) -> ())?
|
||||
|
@ -36,10 +40,14 @@ public class ConfirmationModal: Modal {
|
|||
title: String,
|
||||
explanation: String? = nil,
|
||||
attributedExplanation: NSAttributedString? = nil,
|
||||
accessibilityLabel: String? = nil,
|
||||
accessibilityId: String? = nil,
|
||||
stateToShow: State = .always,
|
||||
confirmTitle: String? = nil,
|
||||
confirmAccessibilityLabel: String? = nil,
|
||||
confirmStyle: ThemeValue = .alert_text,
|
||||
cancelTitle: String = "TXT_CANCEL_TITLE".localized(),
|
||||
cancelAccessibilityLabel: String? = nil,
|
||||
cancelStyle: ThemeValue = .danger,
|
||||
dismissOnConfirm: Bool = true,
|
||||
onConfirm: ((UIViewController) -> ())? = nil,
|
||||
|
@ -48,10 +56,14 @@ public class ConfirmationModal: Modal {
|
|||
self.title = title
|
||||
self.explanation = explanation
|
||||
self.attributedExplanation = attributedExplanation
|
||||
self.accessibilityLabel = accessibilityLabel
|
||||
self.accessibilityId = accessibilityId
|
||||
self.stateToShow = stateToShow
|
||||
self.confirmTitle = confirmTitle
|
||||
self.confirmAccessibilityLabel = confirmAccessibilityLabel
|
||||
self.confirmStyle = confirmStyle
|
||||
self.cancelTitle = cancelTitle
|
||||
self.cancelAccessibilityLabel = cancelAccessibilityLabel
|
||||
self.cancelStyle = cancelStyle
|
||||
self.dismissOnConfirm = dismissOnConfirm
|
||||
self.onConfirm = onConfirm
|
||||
|
@ -67,10 +79,14 @@ public class ConfirmationModal: Modal {
|
|||
return Info(
|
||||
title: self.title,
|
||||
explanation: self.explanation,
|
||||
attributedExplanation: self.attributedExplanation,
|
||||
accessibilityLabel: self.accessibilityLabel,
|
||||
stateToShow: self.stateToShow,
|
||||
confirmTitle: self.confirmTitle,
|
||||
confirmAccessibilityLabel: self.confirmAccessibilityLabel,
|
||||
confirmStyle: self.confirmStyle,
|
||||
cancelTitle: self.cancelTitle,
|
||||
cancelAccessibilityLabel: self.cancelAccessibilityLabel,
|
||||
cancelStyle: self.cancelStyle,
|
||||
dismissOnConfirm: self.dismissOnConfirm,
|
||||
onConfirm: (onConfirm ?? self.onConfirm),
|
||||
|
@ -85,10 +101,13 @@ public class ConfirmationModal: Modal {
|
|||
lhs.title == rhs.title &&
|
||||
lhs.explanation == rhs.explanation &&
|
||||
lhs.attributedExplanation == rhs.attributedExplanation &&
|
||||
lhs.accessibilityLabel == rhs.accessibilityLabel &&
|
||||
lhs.stateToShow == rhs.stateToShow &&
|
||||
lhs.confirmTitle == rhs.confirmTitle &&
|
||||
lhs.confirmAccessibilityLabel == rhs.confirmAccessibilityLabel &&
|
||||
lhs.confirmStyle == rhs.confirmStyle &&
|
||||
lhs.cancelTitle == rhs.cancelTitle &&
|
||||
lhs.cancelAccessibilityLabel == rhs.cancelAccessibilityLabel &&
|
||||
lhs.cancelStyle == rhs.cancelStyle &&
|
||||
lhs.dismissOnConfirm == rhs.dismissOnConfirm
|
||||
)
|
||||
|
@ -98,10 +117,13 @@ public class ConfirmationModal: Modal {
|
|||
title.hash(into: &hasher)
|
||||
explanation.hash(into: &hasher)
|
||||
attributedExplanation.hash(into: &hasher)
|
||||
accessibilityLabel.hash(into: &hasher)
|
||||
stateToShow.hash(into: &hasher)
|
||||
confirmTitle.hash(into: &hasher)
|
||||
confirmAccessibilityLabel.hash(into: &hasher)
|
||||
confirmStyle.hash(into: &hasher)
|
||||
cancelTitle.hash(into: &hasher)
|
||||
cancelAccessibilityLabel.hash(into: &hasher)
|
||||
cancelStyle.hash(into: &hasher)
|
||||
dismissOnConfirm.hash(into: &hasher)
|
||||
}
|
||||
|
@ -207,11 +229,20 @@ public class ConfirmationModal: Modal {
|
|||
info.explanation == nil &&
|
||||
info.attributedExplanation == nil
|
||||
)
|
||||
confirmButton.accessibilityLabel = info.confirmAccessibilityLabel
|
||||
confirmButton.accessibilityIdentifier = info.confirmAccessibilityLabel
|
||||
confirmButton.isAccessibilityElement = true
|
||||
confirmButton.setTitle(info.confirmTitle, for: .normal)
|
||||
confirmButton.setThemeTitleColor(info.confirmStyle, for: .normal)
|
||||
confirmButton.isHidden = (info.confirmTitle == nil)
|
||||
cancelButton.accessibilityLabel = info.cancelAccessibilityLabel
|
||||
cancelButton.accessibilityIdentifier = info.cancelAccessibilityLabel
|
||||
cancelButton.isAccessibilityElement = true
|
||||
cancelButton.setTitle(info.cancelTitle, for: .normal)
|
||||
cancelButton.setThemeTitleColor(info.cancelStyle, for: .normal)
|
||||
|
||||
self.accessibilityLabel = info.accessibilityLabel
|
||||
self.contentView.accessibilityIdentifier = info.accessibilityId
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
|
|
|
@ -17,6 +17,25 @@ public final class SessionButton: UIButton {
|
|||
case large
|
||||
}
|
||||
|
||||
public struct Info {
|
||||
public let style: Style
|
||||
public let title: String
|
||||
public let isEnabled: Bool
|
||||
public let onTap: () -> ()
|
||||
|
||||
public init(
|
||||
style: Style,
|
||||
title: String,
|
||||
isEnabled: Bool,
|
||||
onTap: @escaping () -> ()
|
||||
) {
|
||||
self.style = style
|
||||
self.title = title
|
||||
self.isEnabled = isEnabled
|
||||
self.onTap = onTap
|
||||
}
|
||||
}
|
||||
|
||||
private let style: Style
|
||||
|
||||
public override var isEnabled: Bool {
|
||||
|
@ -157,4 +176,10 @@ public final class SessionButton: UIButton {
|
|||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// MARK: - Functions
|
||||
|
||||
public func setStyle(_ style: Style) {
|
||||
setup(style: style)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ internal enum Theme_ClassicDark: ThemeColors {
|
|||
.messageBubble_outgoingText: .classicDark0,
|
||||
.messageBubble_incomingText: .classicDark6,
|
||||
.messageBubble_overlay: .black_06,
|
||||
.messageBubble_deliveryStatus: .classicDark5,
|
||||
|
||||
// MenuButton
|
||||
.menuButton_background: .primary,
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue