Signal-iOS/SignalUI/ViewControllers/ConversationPicker/ConversationItem.swift
Nora Trapp 7bd167f815
Initial story sending support
* 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
2022-06-10 22:28:03 -04:00

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