* Little fix for context menu * Add 'My Stories' section to stories tab * Add new story thread types * Show stories in conversation picker * Support for sending stories * Update story list when sending stories * Add basic 'My Stories' view controller * Initial stories settings screens * Consolidate TSPrivateStoryThread and TSMyStoryThread into one class * Require an explicit read transaction to initialize an outgoing message * Fix linting * Allow enabling group story from internal settings * Fix tests * PR Feedback
309 lines
9.3 KiB
Swift
309 lines
9.3 KiB
Swift
//
|
|
// Copyright (c) 2022 Open Whisper Systems. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
import SignalMessaging
|
|
|
|
public enum MessageRecipient: Equatable {
|
|
case contact(_ address: SignalServiceAddress)
|
|
case group(_ groupThreadId: String)
|
|
case privateStory(_ storyThreadId: String, isMyStory: Bool)
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
public protocol ConversationItem {
|
|
var messageRecipient: MessageRecipient { get }
|
|
|
|
func title(transaction: SDSAnyReadTransaction) -> String
|
|
|
|
var image: UIImage? { get }
|
|
var isBlocked: Bool { get }
|
|
var disappearingMessagesConfig: OWSDisappearingMessagesConfiguration? { get }
|
|
|
|
func getExistingThread(transaction: SDSAnyReadTransaction) -> TSThread?
|
|
func getOrCreateThread(transaction: SDSAnyWriteTransaction) -> TSThread?
|
|
}
|
|
|
|
extension ConversationItem {
|
|
public var titleWithSneakyTransaction: String { SDSDatabaseStorage.shared.read { title(transaction: $0) } }
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
struct RecentConversationItem {
|
|
enum ItemType {
|
|
case contact(_ item: ContactConversationItem)
|
|
case group(_ item: GroupConversationItem)
|
|
}
|
|
|
|
let backingItem: ItemType
|
|
var unwrapped: ConversationItem {
|
|
switch backingItem {
|
|
case .contact(let contactItem):
|
|
return contactItem
|
|
case .group(let groupItem):
|
|
return groupItem
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
extension RecentConversationItem: ConversationItem {
|
|
var messageRecipient: MessageRecipient {
|
|
return unwrapped.messageRecipient
|
|
}
|
|
|
|
func title(transaction: SDSAnyReadTransaction) -> String {
|
|
return unwrapped.title(transaction: transaction)
|
|
}
|
|
|
|
var image: UIImage? {
|
|
return unwrapped.image
|
|
}
|
|
|
|
var isBlocked: Bool {
|
|
return unwrapped.isBlocked
|
|
}
|
|
|
|
var disappearingMessagesConfig: OWSDisappearingMessagesConfiguration? {
|
|
return unwrapped.disappearingMessagesConfig
|
|
}
|
|
|
|
func getExistingThread(transaction: SDSAnyReadTransaction) -> TSThread? {
|
|
return unwrapped.getExistingThread(transaction: transaction)
|
|
}
|
|
|
|
func getOrCreateThread(transaction: SDSAnyWriteTransaction) -> TSThread? {
|
|
return unwrapped.getOrCreateThread(transaction: transaction)
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
struct ContactConversationItem: Dependencies {
|
|
let address: SignalServiceAddress
|
|
let isBlocked: Bool
|
|
let disappearingMessagesConfig: OWSDisappearingMessagesConfiguration?
|
|
let contactName: String
|
|
let comparableName: String
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
extension ContactConversationItem: Comparable {
|
|
public static func < (lhs: ContactConversationItem, rhs: ContactConversationItem) -> Bool {
|
|
return lhs.comparableName < rhs.comparableName
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
extension ContactConversationItem: ConversationItem {
|
|
|
|
var messageRecipient: MessageRecipient {
|
|
.contact(address)
|
|
}
|
|
|
|
func title(transaction: SDSAnyReadTransaction) -> String {
|
|
guard !address.isLocalAddress else {
|
|
return MessageStrings.noteToSelf
|
|
}
|
|
|
|
return contactName
|
|
}
|
|
|
|
var image: UIImage? {
|
|
databaseStorage.read { transaction in
|
|
self.contactsManagerImpl.avatarImage(forAddress: self.address,
|
|
shouldValidate: true,
|
|
transaction: transaction)
|
|
}
|
|
}
|
|
|
|
func getExistingThread(transaction: SDSAnyReadTransaction) -> TSThread? {
|
|
return TSContactThread.getWithContactAddress(address, transaction: transaction)
|
|
}
|
|
|
|
func getOrCreateThread(transaction: SDSAnyWriteTransaction) -> TSThread? {
|
|
return TSContactThread.getOrCreateThread(withContactAddress: address, transaction: transaction)
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
struct GroupConversationItem: Dependencies {
|
|
let groupThreadId: String
|
|
let isBlocked: Bool
|
|
let disappearingMessagesConfig: OWSDisappearingMessagesConfiguration?
|
|
|
|
// We don't want to keep this in memory, because the group model
|
|
// can be very large.
|
|
var groupThread: TSGroupThread? {
|
|
databaseStorage.read { transaction in
|
|
return TSGroupThread.anyFetchGroupThread(uniqueId: groupThreadId, transaction: transaction)
|
|
}
|
|
}
|
|
|
|
var groupModel: TSGroupModel? {
|
|
groupThread?.groupModel
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
extension GroupConversationItem: ConversationItem {
|
|
var messageRecipient: MessageRecipient {
|
|
.group(groupThreadId)
|
|
}
|
|
|
|
func title(transaction: SDSAnyReadTransaction) -> String {
|
|
guard let groupThread = getExistingThread(transaction: transaction) as? TSGroupThread else {
|
|
return TSGroupThread.defaultGroupName
|
|
}
|
|
return groupThread.groupNameOrDefault
|
|
}
|
|
|
|
var image: UIImage? {
|
|
guard let groupThread = groupThread else { return nil }
|
|
return databaseStorage.read { transaction in
|
|
Self.avatarBuilder.avatarImage(forGroupThread: groupThread,
|
|
diameterPoints: AvatarBuilder.standardAvatarSizePoints,
|
|
transaction: transaction)
|
|
}
|
|
}
|
|
|
|
func getExistingThread(transaction: SDSAnyReadTransaction) -> TSThread? {
|
|
return TSGroupThread.anyFetchGroupThread(uniqueId: groupThreadId, transaction: transaction)
|
|
}
|
|
|
|
func getOrCreateThread(transaction: SDSAnyWriteTransaction) -> TSThread? {
|
|
return getExistingThread(transaction: transaction)
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
struct StoryConversationItem {
|
|
enum ItemType {
|
|
case groupStory(_ item: GroupConversationItem)
|
|
case privateStory(_ item: PrivateStoryConversationItem)
|
|
}
|
|
|
|
let backingItem: ItemType
|
|
var unwrapped: ConversationItem {
|
|
switch backingItem {
|
|
case .groupStory(let item): return item
|
|
case .privateStory(let item): return item
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
extension StoryConversationItem: ConversationItem {
|
|
var messageRecipient: MessageRecipient {
|
|
unwrapped.messageRecipient
|
|
}
|
|
|
|
func title(transaction: SDSAnyReadTransaction) -> String {
|
|
unwrapped.title(transaction: transaction)
|
|
}
|
|
|
|
func subtitle(transaction: SDSAnyReadTransaction) -> String? {
|
|
guard let thread = getExistingThread(transaction: transaction) else {
|
|
owsFailDebug("Unexpectedly missing story thread")
|
|
return nil
|
|
}
|
|
let recipientCount = thread.recipientAddresses(with: transaction).count
|
|
|
|
switch backingItem {
|
|
case .privateStory(let item):
|
|
if item.isMyStory {
|
|
let format = OWSLocalizedString(
|
|
"MY_STORY_VIEWERS_%d",
|
|
tableName: "PluralAware",
|
|
comment: "Format string representing the viewer count for 'My Story'. Embeds {{ number of viewers }}.")
|
|
return String.localizedStringWithFormat(format, recipientCount)
|
|
} else {
|
|
let format = OWSLocalizedString(
|
|
"PRIVATE_STORY_VIEWERS_%d",
|
|
tableName: "PluralAware",
|
|
comment: "Format string representing the viewer count for a custom story list. Embeds {{ number of viewers }}.")
|
|
return String.localizedStringWithFormat(format, recipientCount)
|
|
}
|
|
case .groupStory:
|
|
let format = OWSLocalizedString(
|
|
"GROUP_STORY_VIEWERS_%d",
|
|
tableName: "PluralAware",
|
|
comment: "Format string representing the viewer count for a group story list. Embeds {{ number of viewers }}.")
|
|
return String.localizedStringWithFormat(format, recipientCount)
|
|
}
|
|
}
|
|
|
|
var image: UIImage? {
|
|
unwrapped.image
|
|
}
|
|
|
|
var isBlocked: Bool {
|
|
unwrapped.isBlocked
|
|
}
|
|
|
|
var disappearingMessagesConfig: OWSDisappearingMessagesConfiguration? {
|
|
unwrapped.disappearingMessagesConfig
|
|
}
|
|
|
|
func getExistingThread(transaction: SDSAnyReadTransaction) -> TSThread? {
|
|
unwrapped.getExistingThread(transaction: transaction)
|
|
}
|
|
|
|
func getOrCreateThread(transaction: SDSAnyWriteTransaction) -> TSThread? {
|
|
unwrapped.getOrCreateThread(transaction: transaction)
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
struct PrivateStoryConversationItem: Dependencies {
|
|
let storyThreadId: String
|
|
let isMyStory: Bool
|
|
|
|
var storyThread: TSPrivateStoryThread? {
|
|
databaseStorage.read { transaction in
|
|
return TSPrivateStoryThread.anyFetchPrivateStoryThread(uniqueId: storyThreadId, transaction: transaction)
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
extension PrivateStoryConversationItem: ConversationItem {
|
|
var isBlocked: Bool { false }
|
|
|
|
var disappearingMessagesConfig: OWSDisappearingMessagesConfiguration? { nil }
|
|
|
|
var messageRecipient: MessageRecipient { .privateStory(storyThreadId, isMyStory: isMyStory) }
|
|
|
|
func title(transaction: SDSAnyReadTransaction) -> String {
|
|
guard let storyThread = getExistingThread(transaction: transaction) as? TSPrivateStoryThread else {
|
|
owsFailDebug("Missing story thread")
|
|
return ""
|
|
}
|
|
return storyThread.name
|
|
}
|
|
|
|
var image: UIImage? {
|
|
UIImage(named: "private-story-\(Theme.isDarkThemeEnabled ? "dark" : "light")-36")
|
|
}
|
|
|
|
func getExistingThread(transaction: SDSAnyReadTransaction) -> TSThread? {
|
|
TSPrivateStoryThread.anyFetchPrivateStoryThread(uniqueId: storyThreadId, transaction: transaction)
|
|
}
|
|
|
|
func getOrCreateThread(transaction: SDSAnyWriteTransaction) -> TSThread? {
|
|
getExistingThread(transaction: transaction)
|
|
}
|
|
}
|