mirror of https://github.com/oxen-io/session-ios
Did some more styling and fixed a few UI bugs
Fixed a bug where the time label would no longer appear in the context menu Fixed a bug where the tile label in the context menu could be clipped Tweaked the context menu appearance animation to look less jumpy when at the edges of the screenpull/672/head
parent
fe14bb1b31
commit
7715c5ea09
@ -1,57 +0,0 @@
|
||||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import SessionUIKit
|
||||
import SessionMessagingKit
|
||||
|
||||
final class CallMessageView: UIView {
|
||||
private static let iconSize: CGFloat = 24
|
||||
private static let iconImageViewSize: CGFloat = 40
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
init(cellViewModel: MessageViewModel, textColor: UIColor) {
|
||||
super.init(frame: CGRect.zero)
|
||||
|
||||
setUpViewHierarchy(cellViewModel: cellViewModel, textColor: textColor)
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
preconditionFailure("Use init(viewItem:textColor:) instead.")
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
preconditionFailure("Use init(viewItem:textColor:) instead.")
|
||||
}
|
||||
|
||||
private func setUpViewHierarchy(cellViewModel: MessageViewModel, textColor: UIColor) {
|
||||
// Image view
|
||||
let imageView: UIImageView = UIImageView(
|
||||
image: UIImage(named: "Phone")?
|
||||
.resizedImage(to: CGSize(width: CallMessageView.iconSize, height: CallMessageView.iconSize))?
|
||||
.withRenderingMode(.alwaysTemplate)
|
||||
)
|
||||
imageView.tintColor = textColor
|
||||
imageView.contentMode = .center
|
||||
|
||||
let iconImageViewSize = CallMessageView.iconImageViewSize
|
||||
imageView.set(.width, to: iconImageViewSize)
|
||||
imageView.set(.height, to: iconImageViewSize)
|
||||
|
||||
// Body label
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.lineBreakMode = .byTruncatingTail
|
||||
titleLabel.text = cellViewModel.body
|
||||
titleLabel.textColor = textColor
|
||||
titleLabel.font = .systemFont(ofSize: Values.mediumFontSize)
|
||||
|
||||
// Stack view
|
||||
let stackView = UIStackView(arrangedSubviews: [ imageView, titleLabel ])
|
||||
stackView.axis = .horizontal
|
||||
stackView.alignment = .center
|
||||
stackView.isLayoutMarginsRelativeArrangement = true
|
||||
stackView.layoutMargins = UIEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 12)
|
||||
addSubview(stackView)
|
||||
stackView.pin(to: self, withInset: Values.smallSpacing)
|
||||
}
|
||||
}
|
@ -1,117 +0,0 @@
|
||||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import GRDB
|
||||
import SessionMessagingKit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
final class JoinOpenGroupModal: Modal {
|
||||
private let name: String
|
||||
private let url: String
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
init(name: String?, url: String) {
|
||||
self.name = (name ?? "Open Group")
|
||||
self.url = url
|
||||
|
||||
super.init()
|
||||
}
|
||||
|
||||
override init(afterClosed: (() -> ())? = nil) {
|
||||
preconditionFailure("Use init(name:url:) instead.")
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
preconditionFailure("Use init(name:url:) instead.")
|
||||
}
|
||||
|
||||
override func populateContentView() {
|
||||
// Title
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.textColor = Colors.text
|
||||
titleLabel.font = .boldSystemFont(ofSize: Values.mediumFontSize)
|
||||
titleLabel.text = "Join \(name)?"
|
||||
titleLabel.textAlignment = .center
|
||||
|
||||
// Message
|
||||
let messageLabel = UILabel()
|
||||
messageLabel.textColor = Colors.text
|
||||
messageLabel.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
let message = "Are you sure you want to join the \(name) open group?";
|
||||
let attributedMessage = NSMutableAttributedString(string: message)
|
||||
attributedMessage.addAttributes([ .font : UIFont.boldSystemFont(ofSize: Values.smallFontSize) ], range: (message as NSString).range(of: name))
|
||||
messageLabel.attributedText = attributedMessage
|
||||
messageLabel.numberOfLines = 0
|
||||
messageLabel.lineBreakMode = .byWordWrapping
|
||||
messageLabel.textAlignment = .center
|
||||
|
||||
// Join button
|
||||
let joinButton = UIButton()
|
||||
joinButton.set(.height, to: Values.mediumButtonHeight)
|
||||
joinButton.layer.cornerRadius = Modal.buttonCornerRadius
|
||||
joinButton.backgroundColor = Colors.buttonBackground
|
||||
joinButton.titleLabel!.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
joinButton.setTitleColor(Colors.text, for: UIControl.State.normal)
|
||||
joinButton.setTitle("Join", for: UIControl.State.normal)
|
||||
joinButton.addTarget(self, action: #selector(joinOpenGroup), for: UIControl.Event.touchUpInside)
|
||||
|
||||
// Button stack view
|
||||
let buttonStackView = UIStackView(arrangedSubviews: [ cancelButton, joinButton ])
|
||||
buttonStackView.axis = .horizontal
|
||||
buttonStackView.spacing = Values.mediumSpacing
|
||||
buttonStackView.distribution = .fillEqually
|
||||
|
||||
// Content stack view
|
||||
let contentStackView = UIStackView(arrangedSubviews: [ titleLabel, messageLabel ])
|
||||
contentStackView.axis = .vertical
|
||||
contentStackView.spacing = Values.largeSpacing
|
||||
|
||||
// Main stack view
|
||||
let spacing = Values.largeSpacing - Values.smallFontSize / 2
|
||||
let mainStackView = UIStackView(arrangedSubviews: [ contentStackView, buttonStackView ])
|
||||
mainStackView.axis = .vertical
|
||||
mainStackView.spacing = spacing
|
||||
contentView.addSubview(mainStackView)
|
||||
mainStackView.pin(.leading, to: .leading, of: contentView, withInset: Values.largeSpacing)
|
||||
mainStackView.pin(.top, to: .top, of: contentView, withInset: Values.largeSpacing)
|
||||
contentView.pin(.trailing, to: .trailing, of: mainStackView, withInset: Values.largeSpacing)
|
||||
contentView.pin(.bottom, to: .bottom, of: mainStackView, withInset: spacing)
|
||||
}
|
||||
|
||||
// MARK: - Interaction
|
||||
|
||||
@objc private func joinOpenGroup() {
|
||||
guard let presentingViewController: UIViewController = self.presentingViewController else { return }
|
||||
guard let (room, server, publicKey) = OpenGroupManager.parseOpenGroup(from: url) else {
|
||||
let alert = UIAlertController(title: "Couldn't Join", message: nil, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "BUTTON_OK".localized(), style: .default, handler: nil))
|
||||
|
||||
return presentingViewController.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
presentingViewController.dismiss(animated: true, completion: nil)
|
||||
|
||||
Storage.shared
|
||||
.writeAsync { db in
|
||||
OpenGroupManager.shared.add(
|
||||
db,
|
||||
roomToken: room,
|
||||
server: server,
|
||||
publicKey: publicKey,
|
||||
isConfigMessage: false
|
||||
)
|
||||
}
|
||||
.done(on: DispatchQueue.main) { _ in
|
||||
Storage.shared.writeAsync { db in
|
||||
try MessageSender.syncConfiguration(db, forceSyncNow: true).retainUntilComplete() // FIXME: It's probably cleaner to do this inside addOpenGroup(...)
|
||||
}
|
||||
}
|
||||
.catch(on: DispatchQueue.main) { error in
|
||||
let alert = UIAlertController(title: "Couldn't Join", message: error.localizedDescription, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "BUTTON_OK".localized(), style: .default, handler: nil))
|
||||
presentingViewController.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
.retainUntilComplete()
|
||||
}
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
|
||||
final class SendSeedModal : Modal {
|
||||
var proceed: (() -> Void)? = nil
|
||||
|
||||
private lazy var titleLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.textColor = Colors.text
|
||||
result.font = .boldSystemFont(ofSize: Values.mediumFontSize)
|
||||
result.text = NSLocalizedString("modal_send_seed_title", comment: "")
|
||||
result.textAlignment = .center
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var explanationLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.textColor = Colors.text
|
||||
result.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
result.text = NSLocalizedString("modal_send_seed_explanation", comment: "")
|
||||
result.numberOfLines = 0
|
||||
result.lineBreakMode = .byWordWrapping
|
||||
result.textAlignment = .center
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var sendSeedButton: UIButton = {
|
||||
let result = UIButton()
|
||||
result.set(.height, to: Values.mediumButtonHeight)
|
||||
result.layer.cornerRadius = Modal.buttonCornerRadius
|
||||
if isDarkMode {
|
||||
result.backgroundColor = Colors.destructive
|
||||
}
|
||||
result.titleLabel!.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
result.setTitleColor(isLightMode ? Colors.destructive : Colors.text, for: UIControl.State.normal)
|
||||
result.setTitle(NSLocalizedString("modal_send_seed_send_button_title", comment: ""), for: UIControl.State.normal)
|
||||
result.addTarget(self, action: #selector(sendSeed), for: UIControl.Event.touchUpInside)
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var buttonStackView: UIStackView = {
|
||||
let result = UIStackView(arrangedSubviews: [ cancelButton, sendSeedButton ])
|
||||
result.axis = .horizontal
|
||||
result.spacing = Values.mediumSpacing
|
||||
result.distribution = .fillEqually
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var contentStackView: UIStackView = {
|
||||
let result = UIStackView(arrangedSubviews: [ titleLabel, explanationLabel ])
|
||||
result.axis = .vertical
|
||||
result.spacing = Values.largeSpacing
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var mainStackView: UIStackView = {
|
||||
let result = UIStackView(arrangedSubviews: [ contentStackView, buttonStackView ])
|
||||
result.axis = .vertical
|
||||
result.spacing = Values.largeSpacing - Values.smallFontSize / 2
|
||||
return result
|
||||
}()
|
||||
|
||||
// MARK: Lifecycle
|
||||
override func populateContentView() {
|
||||
contentView.addSubview(mainStackView)
|
||||
mainStackView.pin(.leading, to: .leading, of: contentView, withInset: Values.largeSpacing)
|
||||
mainStackView.pin(.top, to: .top, of: contentView, withInset: Values.largeSpacing)
|
||||
contentView.pin(.trailing, to: .trailing, of: mainStackView, withInset: Values.largeSpacing)
|
||||
contentView.pin(.bottom, to: .bottom, of: mainStackView, withInset: mainStackView.spacing)
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
@objc private func sendSeed() {
|
||||
proceed?()
|
||||
presentingViewController?.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import UIKit
|
||||
import SessionUIKit
|
||||
import SessionMessagingKit
|
||||
|
||||
final class UserDetailsSheet: Sheet {
|
||||
private let profile: Profile
|
||||
|
||||
init(for profile: Profile) {
|
||||
self.profile = profile
|
||||
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
override init(nibName: String?, bundle: Bundle?) {
|
||||
preconditionFailure("Use init(for:) instead.")
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
preconditionFailure("Use init(for:) instead.")
|
||||
}
|
||||
|
||||
override func populateContentView() {
|
||||
// Profile picture view
|
||||
let profilePictureView = ProfilePictureView()
|
||||
let size = Values.largeProfilePictureSize
|
||||
profilePictureView.size = size
|
||||
profilePictureView.set(.width, to: size)
|
||||
profilePictureView.set(.height, to: size)
|
||||
profilePictureView.update(
|
||||
publicKey: profile.id,
|
||||
profile: profile,
|
||||
threadVariant: .contact
|
||||
)
|
||||
|
||||
// Display name label
|
||||
let displayNameLabel = UILabel()
|
||||
let displayName = profile.displayName()
|
||||
displayNameLabel.text = displayName
|
||||
displayNameLabel.font = .boldSystemFont(ofSize: Values.largeFontSize)
|
||||
displayNameLabel.textColor = Colors.text
|
||||
displayNameLabel.numberOfLines = 1
|
||||
displayNameLabel.lineBreakMode = .byTruncatingTail
|
||||
|
||||
// Session ID label
|
||||
let sessionIDLabel = UILabel()
|
||||
sessionIDLabel.textColor = Colors.text
|
||||
sessionIDLabel.font = Fonts.spaceMono(ofSize: isIPhone5OrSmaller ? Values.mediumFontSize : 20)
|
||||
sessionIDLabel.numberOfLines = 0
|
||||
sessionIDLabel.lineBreakMode = .byCharWrapping
|
||||
sessionIDLabel.accessibilityLabel = "Session ID label"
|
||||
sessionIDLabel.text = profile.id
|
||||
|
||||
// Session ID label container
|
||||
let sessionIDLabelContainer = UIView()
|
||||
sessionIDLabelContainer.addSubview(sessionIDLabel)
|
||||
sessionIDLabel.pin(to: sessionIDLabelContainer, withInset: Values.mediumSpacing)
|
||||
sessionIDLabelContainer.layer.cornerRadius = TextField.cornerRadius
|
||||
sessionIDLabelContainer.layer.borderWidth = 1
|
||||
sessionIDLabelContainer.layer.borderColor = isLightMode ? UIColor.black.cgColor : UIColor.white.cgColor
|
||||
|
||||
// Copy button
|
||||
let copyButton = OutlineButton(style: .regular, size: .medium)
|
||||
copyButton.setTitle("copy".localized(), for: .normal)
|
||||
copyButton.addTarget(self, action: #selector(copySessionID), for: UIControl.Event.touchUpInside)
|
||||
copyButton.set(.width, to: 160)
|
||||
|
||||
// Stack view
|
||||
let stackView = UIStackView(arrangedSubviews: [ profilePictureView, displayNameLabel, sessionIDLabelContainer, copyButton, UIView.vSpacer(Values.largeSpacing) ])
|
||||
stackView.axis = .vertical
|
||||
stackView.spacing = Values.largeSpacing
|
||||
stackView.alignment = .center
|
||||
|
||||
// Constraints
|
||||
contentView.addSubview(stackView)
|
||||
stackView.pin(to: contentView, withInset: Values.largeSpacing)
|
||||
}
|
||||
|
||||
@objc private func copySessionID() {
|
||||
UIPasteboard.general.string = profile.id
|
||||
presentingViewController?.dismiss(animated: true, completion: nil)
|
||||
}
|
||||
}
|
@ -0,0 +1,110 @@
|
||||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import GRDB
|
||||
import SessionUtilitiesKit
|
||||
|
||||
public struct MentionInfo: FetchableRecord, Decodable {
|
||||
fileprivate static let threadVariantKey: SQL = SQL(stringLiteral: CodingKeys.threadVariant.stringValue)
|
||||
fileprivate static let openGroupServerKey: SQL = SQL(stringLiteral: CodingKeys.openGroupServer.stringValue)
|
||||
fileprivate static let openGroupRoomTokenKey: SQL = SQL(stringLiteral: CodingKeys.openGroupRoomToken.stringValue)
|
||||
|
||||
fileprivate static let profileString: String = CodingKeys.profile.stringValue
|
||||
|
||||
public let profile: Profile
|
||||
public let threadVariant: SessionThread.Variant
|
||||
public let openGroupServer: String?
|
||||
public let openGroupRoomToken: String?
|
||||
}
|
||||
|
||||
public extension MentionInfo {
|
||||
static func query(
|
||||
userPublicKey: String,
|
||||
threadId: String,
|
||||
threadVariant: SessionThread.Variant,
|
||||
targetPrefix: SessionId.Prefix,
|
||||
pattern: FTS5Pattern?
|
||||
) -> AdaptedFetchRequest<SQLRequest<MentionInfo>>? {
|
||||
guard threadVariant != .contact || userPublicKey != threadId else { return nil }
|
||||
|
||||
let profile: TypedTableAlias<Profile> = TypedTableAlias()
|
||||
let interaction: TypedTableAlias<Interaction> = TypedTableAlias()
|
||||
let openGroup: TypedTableAlias<OpenGroup> = TypedTableAlias()
|
||||
|
||||
let prefixLiteral: SQL = SQL(stringLiteral: "\(targetPrefix.rawValue)%")
|
||||
let profileFullTextSearch: SQL = SQL(stringLiteral: Profile.fullTextSearchTableName)
|
||||
|
||||
/// **Note:** The `\(MentionInfo.profileKey).*` value **MUST** be first
|
||||
let limitSQL: SQL? = (threadVariant == .openGroup ? SQL("LIMIT 20") : nil)
|
||||
|
||||
let request: SQLRequest<MentionInfo> = {
|
||||
guard let pattern: FTS5Pattern = pattern else {
|
||||
return """
|
||||
SELECT
|
||||
\(Profile.self).*,
|
||||
MAX(\(interaction[.timestampMs])), -- Want the newest interaction (for sorting)
|
||||
\(SQL("\(threadVariant) AS \(MentionInfo.threadVariantKey)")),
|
||||
\(openGroup[.server]) AS \(MentionInfo.openGroupServerKey),
|
||||
\(openGroup[.roomToken]) AS \(MentionInfo.openGroupRoomTokenKey)
|
||||
|
||||
FROM \(Profile.self)
|
||||
JOIN \(Interaction.self) ON (
|
||||
\(SQL("\(interaction[.threadId]) = \(threadId)")) AND
|
||||
\(interaction[.authorId]) = \(profile[.id])
|
||||
)
|
||||
LEFT JOIN \(OpenGroup.self) ON \(SQL("\(openGroup[.threadId]) = \(threadId)"))
|
||||
|
||||
WHERE (
|
||||
\(SQL("\(profile[.id]) != \(userPublicKey)")) AND (
|
||||
\(SQL("\(threadVariant) != \(SessionThread.Variant.openGroup)")) OR
|
||||
\(SQL("\(profile[.id]) LIKE '\(prefixLiteral)'"))
|
||||
)
|
||||
)
|
||||
GROUP BY \(profile[.id])
|
||||
ORDER BY \(interaction[.timestampMs].desc)
|
||||
\(limitSQL ?? "")
|
||||
"""
|
||||
}
|
||||
|
||||
// If we do have a search patern then use FTS
|
||||
let matchLiteral: SQL = SQL(stringLiteral: "\(Profile.Columns.nickname.name):\(pattern.rawPattern) OR \(Profile.Columns.name.name):\(pattern.rawPattern)")
|
||||
|
||||
return """
|
||||
SELECT
|
||||
\(Profile.self).*,
|
||||
MAX(\(interaction[.timestampMs])), -- Want the newest interaction (for sorting)
|
||||
\(SQL("\(threadVariant) AS \(MentionInfo.threadVariantKey)")),
|
||||
\(openGroup[.server]) AS \(MentionInfo.openGroupServerKey),
|
||||
\(openGroup[.roomToken]) AS \(MentionInfo.openGroupRoomTokenKey)
|
||||
|
||||
FROM \(profileFullTextSearch)
|
||||
JOIN \(Profile.self) ON (
|
||||
\(Profile.self).rowid = \(profileFullTextSearch).rowid AND
|
||||
\(SQL("\(profile[.id]) != \(userPublicKey)")) AND (
|
||||
\(SQL("\(threadVariant) != \(SessionThread.Variant.openGroup)")) OR
|
||||
\(SQL("\(profile[.id]) LIKE '\(prefixLiteral)'"))
|
||||
)
|
||||
)
|
||||
JOIN \(Interaction.self) ON (
|
||||
\(SQL("\(interaction[.threadId]) = \(threadId)")) AND
|
||||
\(interaction[.authorId]) = \(profile[.id])
|
||||
)
|
||||
LEFT JOIN \(OpenGroup.self) ON \(SQL("\(openGroup[.threadId]) = \(threadId)"))
|
||||
|
||||
WHERE \(profileFullTextSearch) MATCH '\(matchLiteral)'
|
||||
GROUP BY \(profile[.id])
|
||||
ORDER BY \(interaction[.timestampMs].desc)
|
||||
\(limitSQL ?? "")
|
||||
"""
|
||||
}()
|
||||
|
||||
return request.adapted { db in
|
||||
let adapters = try splittingRowAdapters(columnCounts: [
|
||||
Profile.numberOfSelectedColumns(db)
|
||||
])
|
||||
|
||||
return ScopeAdapter([
|
||||
MentionInfo.profileString: adapters[0]
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue