Add support for sending group replies to stories
This commit is contained in:
parent
49d4a6d89f
commit
9494498d70
@ -865,6 +865,7 @@
|
||||
8864072A27F0D426009916B6 /* StoryGroupReplyLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8864072927F0D426009916B6 /* StoryGroupReplyLoader.swift */; };
|
||||
8864072C27F0DA38009916B6 /* StoryGroupReplyViewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8864072B27F0DA37009916B6 /* StoryGroupReplyViewItem.swift */; };
|
||||
8864072E27F0E8DF009916B6 /* StoryGroupReplyCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8864072D27F0E8DF009916B6 /* StoryGroupReplyCell.swift */; };
|
||||
8864073127F21AD7009916B6 /* StoryReplyInputToolbar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8864073027F21AD7009916B6 /* StoryReplyInputToolbar.swift */; };
|
||||
886A58CA276A760600A1099B /* SubscriptionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 886A58C8276A760600A1099B /* SubscriptionManager.swift */; };
|
||||
886A58CB276A760600A1099B /* SubscriptionReceiptCredentialRedemptionJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 886A58C9276A760600A1099B /* SubscriptionReceiptCredentialRedemptionJob.swift */; };
|
||||
886BB3D225BA0C9D00079781 /* PreviewWallpaperViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88ABAB8E25B8BE3F0008C78A /* PreviewWallpaperViewController.swift */; };
|
||||
@ -2095,6 +2096,7 @@
|
||||
8864072927F0D426009916B6 /* StoryGroupReplyLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryGroupReplyLoader.swift; sourceTree = "<group>"; };
|
||||
8864072B27F0DA37009916B6 /* StoryGroupReplyViewItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryGroupReplyViewItem.swift; sourceTree = "<group>"; };
|
||||
8864072D27F0E8DF009916B6 /* StoryGroupReplyCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryGroupReplyCell.swift; sourceTree = "<group>"; };
|
||||
8864073027F21AD7009916B6 /* StoryReplyInputToolbar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoryReplyInputToolbar.swift; sourceTree = "<group>"; };
|
||||
886A58C8276A760600A1099B /* SubscriptionManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionManager.swift; sourceTree = "<group>"; };
|
||||
886A58C9276A760600A1099B /* SubscriptionReceiptCredentialRedemptionJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubscriptionReceiptCredentialRedemptionJob.swift; sourceTree = "<group>"; };
|
||||
886CB07724E77E5B00753909 /* silence.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = silence.aiff; sourceTree = "<group>"; };
|
||||
@ -4055,10 +4057,6 @@
|
||||
884DB94D27DE67BB00C6A309 /* StoryPageViewController.swift */,
|
||||
884DB95127DE67D900C6A309 /* StoryItemMediaView.swift */,
|
||||
884DB95327DEB9E900C6A309 /* StoryPlaybackProgressView.swift */,
|
||||
8864072727EEA658009916B6 /* StoryGroupReplySheet.swift */,
|
||||
8864072927F0D426009916B6 /* StoryGroupReplyLoader.swift */,
|
||||
8864072B27F0DA37009916B6 /* StoryGroupReplyViewItem.swift */,
|
||||
8864072D27F0E8DF009916B6 /* StoryGroupReplyCell.swift */,
|
||||
);
|
||||
path = "Context View";
|
||||
sourceTree = "<group>";
|
||||
@ -4070,6 +4068,7 @@
|
||||
884DB94427DD70F700C6A309 /* IncomingStoryViewModel.swift */,
|
||||
884DB94627DD754700C6A309 /* StoryCell.swift */,
|
||||
885275BF27E26775003F2F9B /* TextAttachmentView.swift */,
|
||||
8864072F27F21AA7009916B6 /* Group Reply Sheet */,
|
||||
884DB94A27DE66E000C6A309 /* Context View */,
|
||||
);
|
||||
path = Stories;
|
||||
@ -4147,6 +4146,18 @@
|
||||
path = Individual;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
8864072F27F21AA7009916B6 /* Group Reply Sheet */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
8864072727EEA658009916B6 /* StoryGroupReplySheet.swift */,
|
||||
8864072927F0D426009916B6 /* StoryGroupReplyLoader.swift */,
|
||||
8864072B27F0DA37009916B6 /* StoryGroupReplyViewItem.swift */,
|
||||
8864072D27F0E8DF009916B6 /* StoryGroupReplyCell.swift */,
|
||||
8864073027F21AD7009916B6 /* StoryReplyInputToolbar.swift */,
|
||||
);
|
||||
path = "Group Reply Sheet";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
8871B16D25F007CE00D4E070 /* Profile */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -6158,6 +6169,7 @@
|
||||
8855DF87238F1E0C0066D96F /* EmojiReactorsTableView.swift in Sources */,
|
||||
3496744F2076ACD000080B5F /* LongTextViewController.swift in Sources */,
|
||||
34F308A21ECB469700BB7697 /* OWSBezierPathView.m in Sources */,
|
||||
8864073127F21AD7009916B6 /* StoryReplyInputToolbar.swift in Sources */,
|
||||
326DF2612739F4D90017B789 /* FeaturedBadgeViewController.swift in Sources */,
|
||||
34ACA7D92733159600E47AD4 /* OnboardingController.swift in Sources */,
|
||||
88A9729422FB4D02004B4FBF /* LocationPicker.swift in Sources */,
|
||||
|
||||
12
Signal/Images.xcassets/add-reaction-outline-24.imageset/Contents.json
vendored
Normal file
12
Signal/Images.xcassets/add-reaction-outline-24.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "add-reaction-outline-24.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
BIN
Signal/Images.xcassets/add-reaction-outline-24.imageset/add-reaction-outline-24.pdf
vendored
Normal file
BIN
Signal/Images.xcassets/add-reaction-outline-24.imageset/add-reaction-outline-24.pdf
vendored
Normal file
Binary file not shown.
@ -1,103 +0,0 @@
|
||||
//
|
||||
// Copyright (c) 2022 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SignalServiceKit
|
||||
|
||||
class StoryGroupReplySheet: InteractiveSheetViewController {
|
||||
private lazy var tableView = UITableView()
|
||||
|
||||
private let storyMessage: StoryMessage
|
||||
|
||||
var dismissHandler: (() -> Void)?
|
||||
|
||||
init(storyMessage: StoryMessage) {
|
||||
self.storyMessage = storyMessage
|
||||
}
|
||||
|
||||
public required init() {
|
||||
fatalError("init() has not been implemented")
|
||||
}
|
||||
|
||||
fileprivate var replyLoader: StoryGroupReplyLoader?
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
tableView.delegate = self
|
||||
tableView.dataSource = self
|
||||
tableView.separatorStyle = .none
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
|
||||
contentView.addSubview(tableView)
|
||||
tableView.autoPinEdgesToSuperviewEdges()
|
||||
|
||||
contentView.backgroundColor = .ows_gray90
|
||||
tableView.backgroundColor = .ows_gray90
|
||||
|
||||
for type in StoryGroupReplyCell.CellType.allCases {
|
||||
tableView.register(StoryGroupReplyCell.self, forCellReuseIdentifier: type.rawValue)
|
||||
}
|
||||
|
||||
replyLoader = StoryGroupReplyLoader(storyMessage: storyMessage, tableView: tableView)
|
||||
}
|
||||
|
||||
override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
|
||||
super.dismiss(animated: flag) { [dismissHandler] in
|
||||
completion?()
|
||||
dismissHandler?()
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
replyLoader?.scrollToBottomOfLoadWindow(animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
extension StoryGroupReplySheet: UIScrollViewDelegate {
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
guard let visibleRows = tableView.indexPathsForVisibleRows?.map({ $0.row }),
|
||||
!visibleRows.isEmpty,
|
||||
let oldestLoadedRow = replyLoader?.oldestLoadedRow,
|
||||
let newestLoadedRow = replyLoader?.newestLoadedRow else { return }
|
||||
|
||||
let rowsFromTop = (visibleRows.min() ?? oldestLoadedRow) - oldestLoadedRow
|
||||
let rowsFromBottom = newestLoadedRow - (visibleRows.max() ?? newestLoadedRow)
|
||||
|
||||
if rowsFromTop <= 30 {
|
||||
replyLoader?.loadOlderPageIfNecessary()
|
||||
}
|
||||
|
||||
if rowsFromBottom <= 30 {
|
||||
replyLoader?.loadNewerPageIfNecessary()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension StoryGroupReplySheet: UITableViewDelegate {
|
||||
|
||||
}
|
||||
|
||||
extension StoryGroupReplySheet: UITableViewDataSource {
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
guard let item = replyLoader?.replyItem(for: indexPath) else {
|
||||
owsFailDebug("Missing item for cell at indexPath \(indexPath)")
|
||||
return UITableViewCell()
|
||||
}
|
||||
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: item.cellType.rawValue, for: indexPath) as! StoryGroupReplyCell
|
||||
cell.configure(with: item)
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
func numberOfSections(in tableView: UITableView) -> Int {
|
||||
1
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
replyLoader?.numberOfRows ?? 0
|
||||
}
|
||||
}
|
||||
@ -8,7 +8,7 @@ import SignalCoreKit
|
||||
class StoryGroupReplyLoader: Dependencies {
|
||||
private let loadingLock = UnfairLock()
|
||||
private var messageMapping: CVMessageMapping!
|
||||
private var threadUniqueId: String!
|
||||
private let threadUniqueId: String
|
||||
private let storyMessage: StoryMessage
|
||||
private weak var tableView: UITableView?
|
||||
private var replyUniqueIds = [String]() {
|
||||
@ -37,18 +37,18 @@ class StoryGroupReplyLoader: Dependencies {
|
||||
return tableView?.indexPathsForVisibleRows?.lazy.map { $0.row }.contains(replyUniqueIds.count - 1) ?? false
|
||||
}
|
||||
|
||||
init?(storyMessage: StoryMessage, tableView: UITableView) {
|
||||
guard case .groupId(let groupId) = storyMessage.context else {
|
||||
owsFailDebug("Unexpected story message context")
|
||||
init?(storyMessage: StoryMessage, threadUniqueId: String?, tableView: UITableView) {
|
||||
guard let threadUniqueId = threadUniqueId else {
|
||||
owsFailDebug("Unexpectedly missing threadUniqueId")
|
||||
return nil
|
||||
}
|
||||
|
||||
self.threadUniqueId = threadUniqueId
|
||||
self.storyMessage = storyMessage
|
||||
self.tableView = tableView
|
||||
|
||||
// Load the first page synchronously.
|
||||
databaseStorage.read { transaction in
|
||||
threadUniqueId = TSGroupThread.threadId(forGroupId: groupId, transaction: transaction)
|
||||
messageMapping = CVMessageMapping(
|
||||
threadUniqueId: threadUniqueId,
|
||||
storyReplyQueryMode: .onlyGroupReplies(storyTimestamp: storyMessage.timestamp)
|
||||
@ -0,0 +1,277 @@
|
||||
//
|
||||
// Copyright (c) 2022 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SignalServiceKit
|
||||
|
||||
class StoryGroupReplySheet: InteractiveSheetViewController {
|
||||
override var renderExternalHandle: Bool { false }
|
||||
|
||||
private lazy var tableView = UITableView()
|
||||
private lazy var inputToolbar = StoryReplyInputToolbar()
|
||||
private lazy var inputToolbarBottomConstraint = inputToolbar.autoPinEdge(toSuperviewEdge: .bottom)
|
||||
|
||||
private lazy var inputAccessoryPlaceholder: InputAccessoryViewPlaceholder = {
|
||||
let placeholder = InputAccessoryViewPlaceholder()
|
||||
placeholder.delegate = self
|
||||
placeholder.referenceView = view
|
||||
return placeholder
|
||||
}()
|
||||
|
||||
private let storyMessage: StoryMessage
|
||||
private let thread: TSThread?
|
||||
|
||||
var dismissHandler: (() -> Void)?
|
||||
|
||||
init(storyMessage: StoryMessage) {
|
||||
self.storyMessage = storyMessage
|
||||
self.thread = Self.databaseStorage.read { transaction in
|
||||
if let groupId = storyMessage.groupId {
|
||||
return TSGroupThread.fetch(groupId: groupId, transaction: transaction)
|
||||
} else {
|
||||
owsFailDebug("Unexpectedly received non-group thread for story reply sheet.")
|
||||
return TSContactThread.getWithContactAddress(storyMessage.authorAddress, transaction: transaction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public required init() {
|
||||
fatalError("init() has not been implemented")
|
||||
}
|
||||
|
||||
fileprivate var replyLoader: StoryGroupReplyLoader?
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
tableView.delegate = self
|
||||
tableView.dataSource = self
|
||||
tableView.separatorStyle = .none
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.keyboardDismissMode = .interactive
|
||||
tableView.contentInset = UIEdgeInsets(top: 30, left: 0, bottom: 0, right: 0)
|
||||
|
||||
contentView.addSubview(tableView)
|
||||
tableView.autoPinEdgesToSuperviewEdges()
|
||||
|
||||
// We add the handle directly to the content view,
|
||||
// so that it doesn't scroll with the table.
|
||||
let handleContainer = UIView()
|
||||
contentView.addSubview(handleContainer)
|
||||
handleContainer.autoPinWidthToSuperview()
|
||||
handleContainer.autoPinEdge(toSuperviewEdge: .top)
|
||||
|
||||
let handle = UIView()
|
||||
handle.backgroundColor = .ows_gray65
|
||||
handle.autoSetDimensions(to: CGSize(width: 36, height: 5))
|
||||
handle.layer.cornerRadius = 5 / 2
|
||||
handleContainer.addSubview(handle)
|
||||
handle.autoPinHeightToSuperview(withMargin: 12)
|
||||
handle.autoHCenterInSuperview()
|
||||
|
||||
inputToolbar.delegate = self
|
||||
contentView.addSubview(inputToolbar)
|
||||
inputToolbar.autoPinWidthToSuperview()
|
||||
inputToolbarBottomConstraint.isActive = true
|
||||
|
||||
contentView.backgroundColor = .ows_gray90
|
||||
tableView.backgroundColor = .ows_gray90
|
||||
handleContainer.backgroundColor = .ows_gray90
|
||||
|
||||
for type in StoryGroupReplyCell.CellType.allCases {
|
||||
tableView.register(StoryGroupReplyCell.self, forCellReuseIdentifier: type.rawValue)
|
||||
}
|
||||
|
||||
replyLoader = StoryGroupReplyLoader(storyMessage: storyMessage, threadUniqueId: thread?.uniqueId, tableView: tableView)
|
||||
}
|
||||
|
||||
public override var canBecomeFirstResponder: Bool { true }
|
||||
|
||||
public override var inputAccessoryView: UIView? { inputAccessoryPlaceholder }
|
||||
|
||||
override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
|
||||
super.dismiss(animated: flag) { [dismissHandler] in
|
||||
completion?()
|
||||
dismissHandler?()
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
replyLoader?.scrollToBottomOfLoadWindow(animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
extension StoryGroupReplySheet: UIScrollViewDelegate {
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
guard let visibleRows = tableView.indexPathsForVisibleRows?.map({ $0.row }),
|
||||
!visibleRows.isEmpty,
|
||||
let oldestLoadedRow = replyLoader?.oldestLoadedRow,
|
||||
let newestLoadedRow = replyLoader?.newestLoadedRow else { return }
|
||||
|
||||
let rowsFromTop = (visibleRows.min() ?? oldestLoadedRow) - oldestLoadedRow
|
||||
let rowsFromBottom = newestLoadedRow - (visibleRows.max() ?? newestLoadedRow)
|
||||
|
||||
if rowsFromTop <= 30 {
|
||||
replyLoader?.loadOlderPageIfNecessary()
|
||||
}
|
||||
|
||||
if rowsFromBottom <= 30 {
|
||||
replyLoader?.loadNewerPageIfNecessary()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension StoryGroupReplySheet: UITableViewDelegate {
|
||||
|
||||
}
|
||||
|
||||
extension StoryGroupReplySheet: UITableViewDataSource {
|
||||
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
guard let item = replyLoader?.replyItem(for: indexPath) else {
|
||||
owsFailDebug("Missing item for cell at indexPath \(indexPath)")
|
||||
return UITableViewCell()
|
||||
}
|
||||
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: item.cellType.rawValue, for: indexPath) as! StoryGroupReplyCell
|
||||
cell.configure(with: item)
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
func numberOfSections(in tableView: UITableView) -> Int {
|
||||
1
|
||||
}
|
||||
|
||||
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
replyLoader?.numberOfRows ?? 0
|
||||
}
|
||||
}
|
||||
|
||||
extension StoryGroupReplySheet: InputAccessoryViewPlaceholderDelegate {
|
||||
public func inputAccessoryPlaceholderKeyboardIsPresenting(animationDuration: TimeInterval, animationCurve: UIView.AnimationCurve) {
|
||||
handleKeyboardStateChange(animationDuration: animationDuration, animationCurve: animationCurve)
|
||||
}
|
||||
|
||||
public func inputAccessoryPlaceholderKeyboardDidPresent() {
|
||||
updateInputToolbarPosition()
|
||||
updateContentInsets(animated: false)
|
||||
}
|
||||
|
||||
public func inputAccessoryPlaceholderKeyboardIsDismissing(animationDuration: TimeInterval, animationCurve: UIView.AnimationCurve) {
|
||||
handleKeyboardStateChange(animationDuration: animationDuration, animationCurve: animationCurve)
|
||||
}
|
||||
|
||||
public func inputAccessoryPlaceholderKeyboardDidDismiss() {
|
||||
updateInputToolbarPosition()
|
||||
updateContentInsets(animated: false)
|
||||
}
|
||||
|
||||
public func inputAccessoryPlaceholderKeyboardIsDismissingInteractively() {
|
||||
updateInputToolbarPosition()
|
||||
}
|
||||
|
||||
func handleKeyboardStateChange(animationDuration: TimeInterval, animationCurve: UIView.AnimationCurve) {
|
||||
guard animationDuration > 0 else {
|
||||
updateInputToolbarPosition()
|
||||
updateContentInsets(animated: false)
|
||||
return
|
||||
}
|
||||
|
||||
UIView.beginAnimations("keyboardStateChange", context: nil)
|
||||
UIView.setAnimationBeginsFromCurrentState(true)
|
||||
UIView.setAnimationCurve(animationCurve)
|
||||
UIView.setAnimationDuration(animationDuration)
|
||||
updateInputToolbarPosition()
|
||||
updateContentInsets(animated: true)
|
||||
UIView.commitAnimations()
|
||||
}
|
||||
|
||||
func updateInputToolbarPosition() {
|
||||
inputToolbarBottomConstraint.constant = -inputAccessoryPlaceholder.keyboardOverlap
|
||||
|
||||
// We always want to apply the new bottom bar position immediately,
|
||||
// as this only happens during animations (interactive or otherwise)
|
||||
inputToolbar.superview?.layoutIfNeeded()
|
||||
}
|
||||
|
||||
func updateContentInsets(animated: Bool) {
|
||||
let wasScrolledToBottom = replyLoader?.isScrolledToBottom ?? false
|
||||
tableView.contentInset.bottom = inputAccessoryPlaceholder.keyboardOverlap + inputToolbar.height - view.safeAreaInsets.bottom
|
||||
if wasScrolledToBottom {
|
||||
replyLoader?.scrollToBottomOfLoadWindow(animated: animated)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension StoryGroupReplySheet: StoryReplyInputToolbarDelegate {
|
||||
func storyReplyInputToolbarDidTapReact(_ storyReplyInputToolbar: StoryReplyInputToolbar) {
|
||||
|
||||
}
|
||||
|
||||
func storyReplyInputToolbarDidTapSend(_ storyReplyInputToolbar: StoryReplyInputToolbar) {
|
||||
guard let messageBody = storyReplyInputToolbar.messageBody, !messageBody.text.isEmpty else {
|
||||
return owsFailDebug("Unexpectedly missing message body")
|
||||
}
|
||||
|
||||
tryToSendTextMessage(messageBody)
|
||||
}
|
||||
|
||||
func tryToSendTextMessage(_ messageBody: MessageBody) {
|
||||
owsAssertDebug(!messageBody.text.isEmpty)
|
||||
|
||||
guard let thread = thread else {
|
||||
return owsFailDebug("Unexpectedly missing thread")
|
||||
}
|
||||
|
||||
guard !blockingManager.isThreadBlocked(thread) else {
|
||||
BlockListUIUtils.showUnblockThreadActionSheet(thread, from: self) { [weak self] isBlocked in
|
||||
guard !isBlocked else { return }
|
||||
self?.tryToSendTextMessage(messageBody)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
guard !SafetyNumberConfirmationSheet.presentIfNecessary(
|
||||
addresses: thread.recipientAddresses,
|
||||
confirmationText: SafetyNumberStrings.confirmSendButton,
|
||||
completion: { [weak self] didConfirmIdentity in
|
||||
guard didConfirmIdentity else { return }
|
||||
self?.tryToSendTextMessage(messageBody)
|
||||
}
|
||||
) else { return }
|
||||
|
||||
let builder = TSOutgoingMessageBuilder(thread: thread)
|
||||
builder.messageBody = messageBody.text
|
||||
builder.bodyRanges = messageBody.ranges
|
||||
builder.storyTimestamp = NSNumber(value: storyMessage.timestamp)
|
||||
builder.storyAuthorAddress = storyMessage.authorAddress
|
||||
let message = builder.build()
|
||||
|
||||
ThreadUtil.enqueueSendAsyncWrite { [weak self] transaction in
|
||||
ThreadUtil.addThreadToProfileWhitelistIfEmptyOrPendingRequest(thread: thread, setDefaultTimerIfNecessary: false, transaction: transaction)
|
||||
|
||||
message.anyInsert(transaction: transaction)
|
||||
|
||||
Self.messageSenderJobQueue.add(message: message.asPreparer, transaction: transaction)
|
||||
|
||||
transaction.addAsyncCompletionOnMain {
|
||||
self?.replyLoader?.reload()
|
||||
self?.inputToolbar.messageBody = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func storyReplyInputToolbarDidBeginEditing(_ storyReplyInputToolbar: StoryReplyInputToolbar) {
|
||||
maximizeHeight()
|
||||
}
|
||||
|
||||
func storyReplyInputToolbarHeightDidChange(_ storyReplyInputToolbar: StoryReplyInputToolbar) {
|
||||
updateContentInsets(animated: false)
|
||||
}
|
||||
|
||||
func storyReplyInputToolbarMentionPickerPossibleAddresses(_ storyReplyInputToolbar: StoryReplyInputToolbar) -> [SignalServiceAddress] {
|
||||
return thread?.recipientAddresses ?? []
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,302 @@
|
||||
//
|
||||
// Copyright (c) 2022 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SignalUI
|
||||
|
||||
// Coincides with Android's max text message length
|
||||
let kMaxMessageBodyCharacterCount = 2000
|
||||
|
||||
protocol StoryReplyInputToolbarDelegate: AnyObject {
|
||||
func storyReplyInputToolbarDidTapSend(_ storyReplyInputToolbar: StoryReplyInputToolbar)
|
||||
func storyReplyInputToolbarDidTapReact(_ storyReplyInputToolbar: StoryReplyInputToolbar)
|
||||
func storyReplyInputToolbarDidBeginEditing(_ storyReplyInputToolbar: StoryReplyInputToolbar)
|
||||
func storyReplyInputToolbarHeightDidChange(_ storyReplyInputToolbar: StoryReplyInputToolbar)
|
||||
func storyReplyInputToolbarMentionPickerPossibleAddresses(_ storyReplyInputToolbar: StoryReplyInputToolbar) -> [SignalServiceAddress]
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
class StoryReplyInputToolbar: UIView {
|
||||
|
||||
weak var delegate: StoryReplyInputToolbarDelegate?
|
||||
|
||||
var messageBody: MessageBody? {
|
||||
get { textView.messageBody }
|
||||
set {
|
||||
textView.messageBody = newValue
|
||||
updateContent()
|
||||
}
|
||||
}
|
||||
|
||||
override var bounds: CGRect {
|
||||
didSet {
|
||||
guard oldValue.height != bounds.height else { return }
|
||||
delegate?.storyReplyInputToolbarHeightDidChange(self)
|
||||
}
|
||||
}
|
||||
|
||||
let minTextViewHeight: CGFloat = 36
|
||||
var maxTextViewHeight: CGFloat {
|
||||
// About ~4 lines in portrait and ~3 lines in landscape.
|
||||
// Otherwise we risk obscuring too much of the content.
|
||||
return UIDevice.current.orientation.isPortrait ? 160 : 100
|
||||
}
|
||||
var textViewHeightConstraint: NSLayoutConstraint?
|
||||
|
||||
// MARK: - Initializers
|
||||
|
||||
init() {
|
||||
super.init(frame: CGRect.zero)
|
||||
|
||||
// When presenting or dismissing the keyboard, there may be a slight
|
||||
// gap between the keyboard and the bottom of the input bar during
|
||||
// the animation. Extend the background below the toolbar's bounds
|
||||
// by this much to mask that extra space.
|
||||
let backgroundExtension: CGFloat = 500
|
||||
|
||||
if UIAccessibility.isReduceTransparencyEnabled {
|
||||
self.backgroundColor = .ows_black
|
||||
|
||||
let extendedBackground = UIView()
|
||||
addSubview(extendedBackground)
|
||||
extendedBackground.autoPinWidthToSuperview()
|
||||
extendedBackground.autoPinEdge(.top, to: .bottom, of: self)
|
||||
extendedBackground.autoSetDimension(.height, toSize: backgroundExtension)
|
||||
} else {
|
||||
self.backgroundColor = .clear
|
||||
|
||||
let blurEffectView = UIVisualEffectView(effect: Theme.darkThemeBarBlurEffect)
|
||||
blurEffectView.layer.zPosition = -1
|
||||
addSubview(blurEffectView)
|
||||
blurEffectView.autoPinWidthToSuperview()
|
||||
blurEffectView.autoPinEdge(toSuperviewEdge: .top)
|
||||
blurEffectView.autoPinEdge(toSuperviewEdge: .bottom, withInset: -backgroundExtension)
|
||||
}
|
||||
|
||||
textView.mentionDelegate = self
|
||||
|
||||
let sendButton = OWSButton.sendButton(imageName: "send-solid-24") { [weak self] in
|
||||
self?.didTapSend()
|
||||
}
|
||||
sendButtonContainer.addSubview(sendButton)
|
||||
|
||||
let reactButton = OWSButton(imageName: "add-reaction-outline-24", tintColor: Theme.darkThemePrimaryColor) { [weak self] in
|
||||
self?.didTapReact()
|
||||
}
|
||||
reactButton.autoSetDimensions(to: CGSize(square: 40))
|
||||
reactButtonContainer.addSubview(reactButton)
|
||||
|
||||
for button in [sendButton, reactButton] {
|
||||
button.autoPinEdgesToSuperviewEdges(with: .zero, excludingEdge: .top)
|
||||
button.autoPinEdge(toSuperviewEdge: .top, withInset: 0, relation: .greaterThanOrEqual)
|
||||
NSLayoutConstraint.autoSetPriority(.defaultLow) {
|
||||
button.autoPinEdge(toSuperviewEdge: .top)
|
||||
}
|
||||
|
||||
button.setContentHuggingHigh()
|
||||
button.setCompressionResistanceHigh()
|
||||
}
|
||||
|
||||
// The input toolbar should *always* be laid out left-to-right, even when using
|
||||
// a right-to-left language. The convention for messaging apps is for the send
|
||||
// button to always be to the right of the input field, even in RTL layouts.
|
||||
// This means, in most places you'll want to pin deliberately to left/right
|
||||
// instead of leading/trailing. You'll also want to the semanticContentAttribute
|
||||
// to ensure horizontal stack views layout left-to-right.
|
||||
|
||||
let hStackView = UIStackView(arrangedSubviews: [ textContainer, sendButtonContainer, reactButtonContainer ])
|
||||
hStackView.isLayoutMarginsRelativeArrangement = true
|
||||
hStackView.layoutMargins = UIEdgeInsets(margin: 12)
|
||||
hStackView.axis = .horizontal
|
||||
hStackView.alignment = .bottom
|
||||
hStackView.spacing = 12
|
||||
hStackView.semanticContentAttribute = .forceLeftToRight
|
||||
|
||||
addSubview(hStackView)
|
||||
hStackView.autoPinEdgesToSuperviewEdges()
|
||||
|
||||
textViewHeightConstraint = textView.autoSetDimension(.height, toSize: minTextViewHeight)
|
||||
|
||||
textContainer.autoPinEdge(toSuperviewMargin: .top)
|
||||
textContainer.autoPinEdge(toSuperviewMargin: .bottom)
|
||||
|
||||
updateContent()
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
notImplemented()
|
||||
}
|
||||
|
||||
// MARK: - UIView Overrides
|
||||
|
||||
override var intrinsicContentSize: CGSize {
|
||||
get {
|
||||
// Since we have `self.autoresizingMask = UIViewAutoresizingFlexibleHeight`, we must specify
|
||||
// an intrinsicContentSize. Specifying CGSize.zero causes the height to be determined by autolayout.
|
||||
return CGSize.zero
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Subviews
|
||||
|
||||
private lazy var sendButtonContainer = UIView()
|
||||
private lazy var reactButtonContainer = UIView()
|
||||
|
||||
lazy var textView: MentionTextView = {
|
||||
let textView = buildTextView()
|
||||
|
||||
textView.scrollIndicatorInsets = UIEdgeInsets(top: 5, left: 0, bottom: 5, right: 3)
|
||||
textView.mentionDelegate = self
|
||||
|
||||
return textView
|
||||
}()
|
||||
|
||||
private let placeholderText = OWSLocalizedString(
|
||||
"MESSAGE_TEXT_FIELD_PLACEHOLDER",
|
||||
comment: "placeholder text for the editable message field"
|
||||
)
|
||||
|
||||
private lazy var placeholderTextView: UITextView = {
|
||||
let placeholderTextView = buildTextView()
|
||||
|
||||
placeholderTextView.text = placeholderText
|
||||
placeholderTextView.isEditable = false
|
||||
placeholderTextView.textContainer.maximumNumberOfLines = 1
|
||||
placeholderTextView.textContainer.lineBreakMode = .byTruncatingTail
|
||||
placeholderTextView.textColor = .ows_whiteAlpha60
|
||||
|
||||
return placeholderTextView
|
||||
}()
|
||||
|
||||
private lazy var textContainer: UIView = {
|
||||
let textContainer = UIView()
|
||||
let textBubble = UIView()
|
||||
textContainer.addSubview(textBubble)
|
||||
let inset = (40 - minTextViewHeight) / 2
|
||||
textBubble.autoPinEdgesToSuperviewEdges(with: UIEdgeInsets(top: 0, leading: 0, bottom: inset, trailing: 0))
|
||||
|
||||
textBubble.layer.cornerRadius = minTextViewHeight / 2
|
||||
textBubble.clipsToBounds = true
|
||||
textBubble.backgroundColor = .ows_gray75
|
||||
|
||||
textBubble.addSubview(placeholderTextView)
|
||||
placeholderTextView.autoPinEdgesToSuperviewEdges()
|
||||
|
||||
textBubble.addSubview(textView)
|
||||
textView.autoPinEdgesToSuperviewEdges()
|
||||
|
||||
return textContainer
|
||||
}()
|
||||
|
||||
private func buildTextView() -> MentionTextView {
|
||||
let textView = MentionTextView()
|
||||
|
||||
textView.keyboardAppearance = Theme.darkThemeKeyboardAppearance
|
||||
textView.backgroundColor = .clear
|
||||
textView.tintColor = Theme.darkThemePrimaryColor
|
||||
|
||||
let textViewFont = UIFont.ows_dynamicTypeBody
|
||||
textView.font = textViewFont
|
||||
textView.textColor = Theme.darkThemePrimaryColor
|
||||
|
||||
// Check the system font size and increase text inset accordingly
|
||||
// to keep the text vertically centered
|
||||
textView.updateVerticalInsetsForDynamicBodyType(defaultInsets: 7)
|
||||
textView.textContainerInset.left = 7
|
||||
textView.textContainerInset.right = 7
|
||||
|
||||
return textView
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
func didTapSend() {
|
||||
textView.acceptAutocorrectSuggestion()
|
||||
delegate?.storyReplyInputToolbarDidTapSend(self)
|
||||
}
|
||||
|
||||
func didTapReact() {
|
||||
delegate?.storyReplyInputToolbarDidTapReact(self)
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func updateContent() {
|
||||
AssertIsOnMainThread()
|
||||
|
||||
updateHeight(textView: textView)
|
||||
|
||||
let isTextViewEmpty = textView.text.isEmptyOrNil
|
||||
|
||||
reactButtonContainer.isHidden = !isTextViewEmpty
|
||||
sendButtonContainer.isHidden = isTextViewEmpty
|
||||
placeholderTextView.isHidden = !isTextViewEmpty
|
||||
}
|
||||
|
||||
private func updateHeight(textView: UITextView) {
|
||||
guard let textViewHeightConstraint = textViewHeightConstraint else {
|
||||
owsFailDebug("Missing constraint.")
|
||||
return
|
||||
}
|
||||
|
||||
// compute new height assuming width is unchanged
|
||||
let currentSize = textView.frame.size
|
||||
let textViewHeight = clampedTextViewHeight(fixedWidth: currentSize.width)
|
||||
|
||||
if textViewHeightConstraint.constant != textViewHeight {
|
||||
Logger.debug("TextView height changed: \(textViewHeightConstraint.constant) -> \(textViewHeight)")
|
||||
textViewHeightConstraint.constant = textViewHeight
|
||||
invalidateIntrinsicContentSize()
|
||||
}
|
||||
}
|
||||
|
||||
private func clampedTextViewHeight(fixedWidth: CGFloat) -> CGFloat {
|
||||
let contentSize = textView.sizeThatFits(CGSize(width: fixedWidth, height: CGFloat.greatestFiniteMagnitude))
|
||||
return CGFloatClamp(contentSize.height, minTextViewHeight, maxTextViewHeight)
|
||||
}
|
||||
}
|
||||
|
||||
extension StoryReplyInputToolbar: MentionTextViewDelegate {
|
||||
func textViewDidBeginTypingMention(_ textView: MentionTextView) {}
|
||||
|
||||
func textViewDidEndTypingMention(_ textView: MentionTextView) {}
|
||||
|
||||
func textViewMentionPickerParentView(_ textView: MentionTextView) -> UIView? {
|
||||
return superview
|
||||
}
|
||||
|
||||
func textViewMentionPickerReferenceView(_ textView: MentionTextView) -> UIView? {
|
||||
return self
|
||||
}
|
||||
|
||||
func textViewMentionPickerPossibleAddresses(_ textView: MentionTextView) -> [SignalServiceAddress] {
|
||||
return delegate?.storyReplyInputToolbarMentionPickerPossibleAddresses(self) ?? []
|
||||
}
|
||||
|
||||
func textView(_ textView: MentionTextView, didDeleteMention mention: Mention) {}
|
||||
|
||||
func textView(_ textView: MentionTextView, shouldResolveMentionForAddress address: SignalServiceAddress) -> Bool {
|
||||
return textViewMentionPickerPossibleAddresses(textView).contains(address)
|
||||
}
|
||||
|
||||
func textViewMentionStyle(_ textView: MentionTextView) -> Mention.Style {
|
||||
return .groupReply
|
||||
}
|
||||
|
||||
public func textViewDidChange(_ textView: UITextView) {
|
||||
updateHeight(textView: textView)
|
||||
updateContent()
|
||||
}
|
||||
|
||||
public func textViewDidBeginEditing(_ textView: UITextView) {
|
||||
delegate?.storyReplyInputToolbarDidBeginEditing(self)
|
||||
updateContent()
|
||||
}
|
||||
|
||||
public func textViewDidEndEditing(_ textView: UITextView) {
|
||||
updateContent()
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
|
||||
// Copyright (c) 2022 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
@ -47,6 +47,11 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
transaction:(SDSAnyWriteTransaction *)transaction
|
||||
NS_SWIFT_NAME(addThreadToProfileWhitelistIfEmptyOrPendingRequestAndSetDefaultTimer(thread:transaction:));
|
||||
|
||||
+ (BOOL)addThreadToProfileWhitelistIfEmptyOrPendingRequest:(TSThread *)thread
|
||||
setDefaultTimerIfNecessary:(BOOL)setDefaultTimerIfNecessary
|
||||
transaction:(SDSAnyWriteTransaction *)transaction
|
||||
NS_SWIFT_NAME(addThreadToProfileWhitelistIfEmptyOrPendingRequest(thread:setDefaultTimerIfNecessary:transaction:));
|
||||
|
||||
#pragma mark - Delete Content
|
||||
|
||||
+ (void)deleteAllContent;
|
||||
|
||||
@ -182,6 +182,15 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
+ (BOOL)addThreadToProfileWhitelistIfEmptyOrPendingRequestAndSetDefaultTimer:(TSThread *)thread
|
||||
transaction:(SDSAnyWriteTransaction *)transaction
|
||||
{
|
||||
return [self addThreadToProfileWhitelistIfEmptyOrPendingRequest:thread
|
||||
setDefaultTimerIfNecessary:YES
|
||||
transaction:transaction];
|
||||
}
|
||||
|
||||
+ (BOOL)addThreadToProfileWhitelistIfEmptyOrPendingRequest:(TSThread *)thread
|
||||
setDefaultTimerIfNecessary:(BOOL)setDefaultTimerIfNecessary
|
||||
transaction:(SDSAnyWriteTransaction *)transaction
|
||||
{
|
||||
OWSAssertDebug(thread);
|
||||
|
||||
@ -192,7 +201,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
[GRDBThreadFinder shouldSetDefaultDisappearingMessageTimerWithThread:thread
|
||||
transaction:transaction.unwrapGrdbRead];
|
||||
|
||||
if (needsDefaultTimerSet) {
|
||||
if (needsDefaultTimerSet && setDefaultTimerIfNecessary) {
|
||||
OWSDisappearingMessagesConfiguration *configuration =
|
||||
[OWSDisappearingMessagesConfiguration applyToken:defaultTimerToken toThread:thread transaction:transaction];
|
||||
|
||||
|
||||
@ -116,7 +116,9 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
messageSticker:transcript.messageSticker
|
||||
isViewOnceMessage:transcript.isViewOnceMessage
|
||||
changeActionsProtoData:nil
|
||||
additionalRecipients:nil] build];
|
||||
additionalRecipients:nil
|
||||
storyAuthorAddress:nil
|
||||
storyTimestamp:nil] build];
|
||||
|
||||
SignalServiceAddress *_Nullable localAddress = self.tsAccountManager.localAddress;
|
||||
if (localAddress == nil) {
|
||||
|
||||
@ -1089,6 +1089,21 @@ NSUInteger const TSOutgoingMessageSchemaVersion = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Story Context
|
||||
if (self.storyTimestamp && self.storyAuthorUuidString) {
|
||||
SSKProtoDataMessageStoryContextBuilder *storyContextBuilder = [SSKProtoDataMessageStoryContext builder];
|
||||
[storyContextBuilder setAuthorUuid:self.storyAuthorUuidString];
|
||||
[storyContextBuilder setSentTimestamp:self.storyTimestamp.longLongValue];
|
||||
|
||||
NSError *error;
|
||||
SSKProtoDataMessageStoryContext *_Nullable storyContext = [storyContextBuilder buildAndReturnError:&error];
|
||||
if (error || !storyContext) {
|
||||
OWSFailDebug(@"Could not build storyContext protobuf: %@.", error);
|
||||
} else {
|
||||
[builder setStoryContext:storyContext];
|
||||
}
|
||||
}
|
||||
|
||||
[builder setExpireTimer:self.expiresInSeconds];
|
||||
|
||||
// Group Messages
|
||||
|
||||
@ -36,7 +36,9 @@ public class TSOutgoingMessageBuilder: TSMessageBuilder {
|
||||
messageSticker: MessageSticker? = nil,
|
||||
isViewOnceMessage: Bool = false,
|
||||
changeActionsProtoData: Data? = nil,
|
||||
additionalRecipients: [SignalServiceAddress]? = nil) {
|
||||
additionalRecipients: [SignalServiceAddress]? = nil,
|
||||
storyAuthorAddress: SignalServiceAddress? = nil,
|
||||
storyTimestamp: UInt64? = nil) {
|
||||
|
||||
super.init(thread: thread,
|
||||
timestamp: timestamp,
|
||||
@ -49,7 +51,9 @@ public class TSOutgoingMessageBuilder: TSMessageBuilder {
|
||||
contactShare: contactShare,
|
||||
linkPreview: linkPreview,
|
||||
messageSticker: messageSticker,
|
||||
isViewOnceMessage: isViewOnceMessage)
|
||||
isViewOnceMessage: isViewOnceMessage,
|
||||
storyAuthorAddress: storyAuthorAddress,
|
||||
storyTimestamp: storyTimestamp)
|
||||
|
||||
self.isVoiceMessage = isVoiceMessage
|
||||
self.groupMetaMessage = groupMetaMessage
|
||||
@ -88,7 +92,9 @@ public class TSOutgoingMessageBuilder: TSMessageBuilder {
|
||||
messageSticker: MessageSticker?,
|
||||
isViewOnceMessage: Bool,
|
||||
changeActionsProtoData: Data?,
|
||||
additionalRecipients: [SignalServiceAddress]?) -> TSOutgoingMessageBuilder {
|
||||
additionalRecipients: [SignalServiceAddress]?,
|
||||
storyAuthorAddress: SignalServiceAddress?,
|
||||
storyTimestamp: NSNumber?) -> TSOutgoingMessageBuilder {
|
||||
return TSOutgoingMessageBuilder(thread: thread,
|
||||
timestamp: timestamp,
|
||||
messageBody: messageBody,
|
||||
@ -104,7 +110,9 @@ public class TSOutgoingMessageBuilder: TSMessageBuilder {
|
||||
messageSticker: messageSticker,
|
||||
isViewOnceMessage: isViewOnceMessage,
|
||||
changeActionsProtoData: changeActionsProtoData,
|
||||
additionalRecipients: additionalRecipients)
|
||||
additionalRecipients: additionalRecipients,
|
||||
storyAuthorAddress: storyAuthorAddress,
|
||||
storyTimestamp: storyTimestamp?.uint64Value)
|
||||
}
|
||||
|
||||
private var hasBuilt = false
|
||||
|
||||
@ -1519,6 +1519,7 @@ public class GRDBInteractionFinder: NSObject, InteractionFinderAdapter {
|
||||
AND \(interactionColumn: .errorType) IN (\(errorMessageTypes.map { "\($0.rawValue)" }.joined(separator: ",")))
|
||||
) OR \(interactionColumn: .recordType) IN (\(interactionTypes.map { "\($0.rawValue)" }.joined(separator: ",")))
|
||||
)
|
||||
\(Self.filterStoryRepliesClause(for: .excludeGroupReplies))
|
||||
LIMIT 1
|
||||
)
|
||||
"""
|
||||
|
||||
@ -139,7 +139,9 @@ public class OutgoingMessageFactory: NSObject, Factory {
|
||||
messageSticker: messageStickerBuilder(),
|
||||
isViewOnceMessage: isViewOnceMessageBuilder(),
|
||||
changeActionsProtoData: changeActionsProtoDataBuilder(),
|
||||
additionalRecipients: additionalRecipientsBuilder()).build()
|
||||
additionalRecipients: additionalRecipientsBuilder(),
|
||||
storyAuthorAddress: storyAuthorAddressBuilder(),
|
||||
storyTimestamp: storyTimestampBuilder()).build()
|
||||
}
|
||||
|
||||
@objc
|
||||
@ -234,6 +236,16 @@ public class OutgoingMessageFactory: NSObject, Factory {
|
||||
return nil
|
||||
}
|
||||
|
||||
@objc
|
||||
public var storyAuthorAddressBuilder: () -> SignalServiceAddress? = {
|
||||
return nil
|
||||
}
|
||||
|
||||
@objc
|
||||
public var storyTimestampBuilder: () -> NSNumber? = {
|
||||
return nil
|
||||
}
|
||||
|
||||
// MARK: Delivery Receipts
|
||||
|
||||
@objc
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
//
|
||||
// Copyright (c) 2021 Open Whisper Systems. All rights reserved.
|
||||
// Copyright (c) 2022 Open Whisper Systems. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
@ -36,7 +36,7 @@ public class InputAccessoryViewPlaceholder: UIView {
|
||||
}
|
||||
|
||||
@objc
|
||||
weak var referenceView: UIView?
|
||||
public weak var referenceView: UIView?
|
||||
|
||||
private var visibleKeyboardHeight: CGFloat {
|
||||
guard var keyboardFrame = transitioningKeyboardFrame ?? superview?.frame else { return 0 }
|
||||
|
||||
@ -159,6 +159,22 @@ class MentionPicker: UIView {
|
||||
if style == .composingAttachment {
|
||||
tableView.backgroundColor = UIColor.ows_gray95
|
||||
hairlineView.backgroundColor = .ows_gray65
|
||||
} else if style == .groupReply {
|
||||
blurView?.removeFromSuperview()
|
||||
blurView = nil
|
||||
|
||||
if UIAccessibility.isReduceTransparencyEnabled {
|
||||
tableView.backgroundColor = Theme.darkThemeBackgroundColor
|
||||
} else {
|
||||
tableView.backgroundColor = .clear
|
||||
|
||||
let blurView = UIVisualEffectView(effect: Theme.darkThemeBarBlurEffect)
|
||||
self.blurView = blurView
|
||||
insertSubview(blurView, belowSubview: tableView)
|
||||
blurView.autoPinEdgesToSuperviewEdges()
|
||||
}
|
||||
|
||||
hairlineView.backgroundColor = .ows_gray75
|
||||
} else {
|
||||
blurView?.removeFromSuperview()
|
||||
blurView = nil
|
||||
@ -330,7 +346,7 @@ private class MentionableUserCell: UITableViewCell {
|
||||
}
|
||||
|
||||
func configure(with mentionableUser: MentionPicker.MentionableUser, style: Mention.Style) {
|
||||
if style == .composingAttachment {
|
||||
if [.composingAttachment, .groupReply].contains(style) {
|
||||
displayNameLabel.textColor = Theme.darkThemePrimaryColor
|
||||
usernameLabel.textColor = Theme.darkThemeSecondaryTextAndIconColor
|
||||
selectedBackgroundView?.backgroundColor = UIColor.white.withAlphaComponent(0.2)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user