Conversations
This commit is contained in:
parent
407bc00ee0
commit
ecb2197a07
|
@ -105,6 +105,21 @@ extension ContentDatabase {
|
||||||
t.column("wholeWord", .boolean).notNull()
|
t.column("wholeWord", .boolean).notNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try db.create(table: "conversationRecord") { t in
|
||||||
|
t.column("id", .text).primaryKey(onConflict: .replace)
|
||||||
|
t.column("unread", .boolean).notNull()
|
||||||
|
t.column("lastStatusId", .text).references("statusRecord")
|
||||||
|
}
|
||||||
|
|
||||||
|
try db.create(table: "conversationAccountJoin") { t in
|
||||||
|
t.column("conversationId", .text).indexed().notNull()
|
||||||
|
.references("conversationRecord", onDelete: .cascade)
|
||||||
|
t.column("accountId", .text).indexed().notNull()
|
||||||
|
.references("accountRecord", onDelete: .cascade)
|
||||||
|
|
||||||
|
t.primaryKey(["conversationId", "accountId"], onConflict: .replace)
|
||||||
|
}
|
||||||
|
|
||||||
try db.create(table: "lastReadIdRecord") { t in
|
try db.create(table: "lastReadIdRecord") { t in
|
||||||
t.column("markerTimeline", .text).primaryKey(onConflict: .replace)
|
t.column("markerTimeline", .text).primaryKey(onConflict: .replace)
|
||||||
t.column("id", .text).notNull()
|
t.column("id", .text).notNull()
|
||||||
|
|
|
@ -302,6 +302,16 @@ public extension ContentDatabase {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func insert(conversations: [Conversation]) -> AnyPublisher<Never, Error> {
|
||||||
|
databaseWriter.writePublisher {
|
||||||
|
for conversation in conversations {
|
||||||
|
try conversation.save($0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ignoreOutput()
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
func timelinePublisher(_ timeline: Timeline) -> AnyPublisher<[[CollectionItem]], Error> {
|
func timelinePublisher(_ timeline: Timeline) -> AnyPublisher<[[CollectionItem]], Error> {
|
||||||
ValueObservation.tracking(
|
ValueObservation.tracking(
|
||||||
TimelineItemsInfo.request(TimelineRecord.filter(TimelineRecord.Columns.id == timeline.id)).fetchOne)
|
TimelineItemsInfo.request(TimelineRecord.filter(TimelineRecord.Columns.id == timeline.id)).fetchOne)
|
||||||
|
@ -378,6 +388,17 @@ public extension ContentDatabase {
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func conversationsPublisher() -> AnyPublisher<[Conversation], Error> {
|
||||||
|
ValueObservation.tracking(ConversationInfo.request(ConversationRecord.all()).fetchAll)
|
||||||
|
.removeDuplicates()
|
||||||
|
.publisher(in: databaseWriter)
|
||||||
|
.map {
|
||||||
|
$0.sorted { $0.lastStatusInfo.record.createdAt > $1.lastStatusInfo.record.createdAt }
|
||||||
|
.map(Conversation.init(info:))
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
|
||||||
func lastReadId(_ markerTimeline: Marker.Timeline) -> String? {
|
func lastReadId(_ markerTimeline: Marker.Timeline) -> String? {
|
||||||
try? databaseWriter.read {
|
try? databaseWriter.read {
|
||||||
try String.fetchOne(
|
try String.fetchOne(
|
||||||
|
@ -402,6 +423,8 @@ private extension ContentDatabase {
|
||||||
let notificationAccountIds: [Account.Id]
|
let notificationAccountIds: [Account.Id]
|
||||||
let notificationStatusIds: [Status.Id]
|
let notificationStatusIds: [Status.Id]
|
||||||
|
|
||||||
|
try ConversationRecord.deleteAll($0)
|
||||||
|
|
||||||
if useNotificationsLastReadId {
|
if useNotificationsLastReadId {
|
||||||
var notificationIds = try MastodonNotification.Id.fetchAll(
|
var notificationIds = try MastodonNotification.Id.fetchAll(
|
||||||
$0,
|
$0,
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import GRDB
|
||||||
|
import Mastodon
|
||||||
|
|
||||||
|
struct ConversationAccountJoin: ContentDatabaseRecord {
|
||||||
|
let conversationId: Conversation.Id
|
||||||
|
let accountId: Account.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ConversationAccountJoin {
|
||||||
|
enum Columns {
|
||||||
|
static let conversationId = Column(ConversationAccountJoin.CodingKeys.conversationId)
|
||||||
|
static let accountId = Column(ConversationAccountJoin.CodingKeys.accountId)
|
||||||
|
}
|
||||||
|
|
||||||
|
static let account = belongsTo(AccountRecord.self)
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import GRDB
|
||||||
|
|
||||||
|
struct ConversationInfo: Codable, Hashable, FetchableRecord {
|
||||||
|
let record: ConversationRecord
|
||||||
|
let accountInfos: [AccountInfo]
|
||||||
|
let lastStatusInfo: StatusInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ConversationInfo {
|
||||||
|
static func addingIncludes<T: DerivableRequest>(_ request: T) -> T where T.RowDecoder == ConversationRecord {
|
||||||
|
request.including(all: AccountInfo.addingIncludes(ConversationRecord.accounts).forKey(CodingKeys.accountInfos))
|
||||||
|
.including(required: StatusInfo.addingIncludes(ConversationRecord.lastStatus)
|
||||||
|
.forKey(CodingKeys.lastStatusInfo))
|
||||||
|
}
|
||||||
|
|
||||||
|
static func request(_ request: QueryInterfaceRequest<ConversationRecord>) -> QueryInterfaceRequest<Self> {
|
||||||
|
addingIncludes(request).asRequest(of: self)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import GRDB
|
||||||
|
import Mastodon
|
||||||
|
|
||||||
|
struct ConversationRecord: ContentDatabaseRecord, Hashable {
|
||||||
|
let id: Conversation.Id
|
||||||
|
let unread: Bool
|
||||||
|
let lastStatusId: Status.Id?
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ConversationRecord {
|
||||||
|
enum Columns {
|
||||||
|
static let id = Column(ConversationRecord.CodingKeys.id)
|
||||||
|
static let unread = Column(ConversationRecord.CodingKeys.unread)
|
||||||
|
static let lastStatusId = Column(ConversationRecord.CodingKeys.lastStatusId)
|
||||||
|
}
|
||||||
|
|
||||||
|
static let lastStatus = belongsTo(StatusRecord.self)
|
||||||
|
static let accountJoins = hasMany(ConversationAccountJoin.self)
|
||||||
|
static let accounts = hasMany(
|
||||||
|
AccountRecord.self,
|
||||||
|
through: accountJoins,
|
||||||
|
using: ConversationAccountJoin.account)
|
||||||
|
|
||||||
|
init(conversation: Conversation) {
|
||||||
|
id = conversation.id
|
||||||
|
unread = conversation.unread
|
||||||
|
lastStatusId = conversation.lastStatus?.id
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ public enum CollectionItem: Hashable {
|
||||||
case loadMore(LoadMore)
|
case loadMore(LoadMore)
|
||||||
case account(Account)
|
case account(Account)
|
||||||
case notification(MastodonNotification, StatusConfiguration?)
|
case notification(MastodonNotification, StatusConfiguration?)
|
||||||
|
case conversation(Conversation)
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension CollectionItem {
|
public extension CollectionItem {
|
||||||
|
@ -45,6 +46,8 @@ public extension CollectionItem {
|
||||||
return account.id
|
return account.id
|
||||||
case let .notification(notification, _):
|
case let .notification(notification, _):
|
||||||
return notification.id
|
return notification.id
|
||||||
|
case let .conversation(conversation):
|
||||||
|
return conversation.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import GRDB
|
||||||
|
import Mastodon
|
||||||
|
|
||||||
|
extension Conversation {
|
||||||
|
func save(_ db: Database) throws {
|
||||||
|
guard let lastStatus = lastStatus else { return }
|
||||||
|
|
||||||
|
try lastStatus.save(db)
|
||||||
|
try ConversationRecord(conversation: self).save(db)
|
||||||
|
|
||||||
|
for account in accounts {
|
||||||
|
try account.save(db)
|
||||||
|
try ConversationAccountJoin(conversationId: id, accountId: account.id).save(db)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init(info: ConversationInfo) {
|
||||||
|
self.init(
|
||||||
|
id: info.record.id,
|
||||||
|
accounts: info.accountInfos.map(Account.init(info:)),
|
||||||
|
unread: info.record.unread,
|
||||||
|
lastStatus: Status(info: info.lastStatusInfo))
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,6 +26,8 @@ final class TableViewDataSource: UITableViewDiffableDataSource<Int, CollectionIt
|
||||||
loadMoreCell.viewModel = loadMoreViewModel
|
loadMoreCell.viewModel = loadMoreViewModel
|
||||||
case let (notificationListCell as NotificationListCell, notificationViewModel as NotificationViewModel):
|
case let (notificationListCell as NotificationListCell, notificationViewModel as NotificationViewModel):
|
||||||
notificationListCell.viewModel = notificationViewModel
|
notificationListCell.viewModel = notificationViewModel
|
||||||
|
case let (conversationListCell as ConversationListCell, conversationViewModel as ConversationViewModel):
|
||||||
|
conversationListCell.viewModel = conversationViewModel
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,8 @@ extension CollectionItem {
|
||||||
StatusListCell.self,
|
StatusListCell.self,
|
||||||
AccountListCell.self,
|
AccountListCell.self,
|
||||||
LoadMoreCell.self,
|
LoadMoreCell.self,
|
||||||
NotificationListCell.self]
|
NotificationListCell.self,
|
||||||
|
ConversationListCell.self]
|
||||||
|
|
||||||
var cellClass: AnyClass {
|
var cellClass: AnyClass {
|
||||||
switch self {
|
switch self {
|
||||||
|
@ -20,6 +21,8 @@ extension CollectionItem {
|
||||||
return LoadMoreCell.self
|
return LoadMoreCell.self
|
||||||
case let .notification(_, statusConfiguration):
|
case let .notification(_, statusConfiguration):
|
||||||
return statusConfiguration == nil ? NotificationListCell.self : StatusListCell.self
|
return statusConfiguration == nil ? NotificationListCell.self : StatusListCell.self
|
||||||
|
case .conversation:
|
||||||
|
return ConversationListCell.self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
"identities.pending" = "Pending";
|
"identities.pending" = "Pending";
|
||||||
"lists.new-list-title" = "New List Title";
|
"lists.new-list-title" = "New List Title";
|
||||||
"load-more" = "Load More";
|
"load-more" = "Load More";
|
||||||
|
"messages" = "Messages";
|
||||||
"pending.pending-confirmation" = "Your account is pending confirmation";
|
"pending.pending-confirmation" = "Your account is pending confirmation";
|
||||||
"preferences" = "Preferences";
|
"preferences" = "Preferences";
|
||||||
"preferences.app" = "App Preferences";
|
"preferences.app" = "App Preferences";
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct Conversation: Codable, Hashable {
|
||||||
|
public let id: Id
|
||||||
|
public let accounts: [Account]
|
||||||
|
public let unread: Bool
|
||||||
|
public let lastStatus: Status?
|
||||||
|
|
||||||
|
public init(id: String, accounts: [Account], unread: Bool, lastStatus: Status?) {
|
||||||
|
self.id = id
|
||||||
|
self.accounts = accounts
|
||||||
|
self.unread = unread
|
||||||
|
self.lastStatus = lastStatus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension Conversation {
|
||||||
|
typealias Id = String
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import HTTP
|
||||||
|
import Mastodon
|
||||||
|
|
||||||
|
public enum ConversationsEndpoint {
|
||||||
|
case conversations
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ConversationsEndpoint: Endpoint {
|
||||||
|
public typealias ResultType = [Conversation]
|
||||||
|
|
||||||
|
public var pathComponentsInContext: [String] {
|
||||||
|
["conversations"]
|
||||||
|
}
|
||||||
|
|
||||||
|
public var method: HTTPMethod { .get }
|
||||||
|
}
|
|
@ -8,6 +8,10 @@
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
D0030982250C6C8500EACB32 /* URL+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0030981250C6C8500EACB32 /* URL+Extensions.swift */; };
|
D0030982250C6C8500EACB32 /* URL+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0030981250C6C8500EACB32 /* URL+Extensions.swift */; };
|
||||||
|
D00702292555E51200F38136 /* ConversationListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00702282555E51200F38136 /* ConversationListCell.swift */; };
|
||||||
|
D00702312555F4AE00F38136 /* ConversationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00702302555F4AE00F38136 /* ConversationView.swift */; };
|
||||||
|
D00702362555F4C500F38136 /* ConversationContentConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00702352555F4C500F38136 /* ConversationContentConfiguration.swift */; };
|
||||||
|
D007023E25562A2800F38136 /* ConversationAvatarsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D007023D25562A2800F38136 /* ConversationAvatarsView.swift */; };
|
||||||
D00CB2ED2533ACC00080096B /* StatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00CB2EC2533ACC00080096B /* StatusView.swift */; };
|
D00CB2ED2533ACC00080096B /* StatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D00CB2EC2533ACC00080096B /* StatusView.swift */; };
|
||||||
D01C6FAC252024BD003D0300 /* Array+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01C6FAB252024BD003D0300 /* Array+Extensions.swift */; };
|
D01C6FAC252024BD003D0300 /* Array+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01C6FAB252024BD003D0300 /* Array+Extensions.swift */; };
|
||||||
D01EF22425182B1F00650C6B /* AccountHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01EF22325182B1F00650C6B /* AccountHeaderView.swift */; };
|
D01EF22425182B1F00650C6B /* AccountHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D01EF22325182B1F00650C6B /* AccountHeaderView.swift */; };
|
||||||
|
@ -116,6 +120,10 @@
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
D0030981250C6C8500EACB32 /* URL+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Extensions.swift"; sourceTree = "<group>"; };
|
D0030981250C6C8500EACB32 /* URL+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
|
D00702282555E51200F38136 /* ConversationListCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationListCell.swift; sourceTree = "<group>"; };
|
||||||
|
D00702302555F4AE00F38136 /* ConversationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationView.swift; sourceTree = "<group>"; };
|
||||||
|
D00702352555F4C500F38136 /* ConversationContentConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationContentConfiguration.swift; sourceTree = "<group>"; };
|
||||||
|
D007023D25562A2800F38136 /* ConversationAvatarsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationAvatarsView.swift; sourceTree = "<group>"; };
|
||||||
D00CB2EC2533ACC00080096B /* StatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusView.swift; sourceTree = "<group>"; };
|
D00CB2EC2533ACC00080096B /* StatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusView.swift; sourceTree = "<group>"; };
|
||||||
D01C6FAB252024BD003D0300 /* Array+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Extensions.swift"; sourceTree = "<group>"; };
|
D01C6FAB252024BD003D0300 /* Array+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
D01EF22325182B1F00650C6B /* AccountHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountHeaderView.swift; sourceTree = "<group>"; };
|
D01EF22325182B1F00650C6B /* AccountHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountHeaderView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -338,6 +346,10 @@
|
||||||
D0F0B125251A90F400942152 /* AccountListCell.swift */,
|
D0F0B125251A90F400942152 /* AccountListCell.swift */,
|
||||||
D0F0B10D251A868200942152 /* AccountView.swift */,
|
D0F0B10D251A868200942152 /* AccountView.swift */,
|
||||||
D0C7D42424F76169001EBDBB /* AddIdentityView.swift */,
|
D0C7D42424F76169001EBDBB /* AddIdentityView.swift */,
|
||||||
|
D007023D25562A2800F38136 /* ConversationAvatarsView.swift */,
|
||||||
|
D00702352555F4C500F38136 /* ConversationContentConfiguration.swift */,
|
||||||
|
D00702282555E51200F38136 /* ConversationListCell.swift */,
|
||||||
|
D00702302555F4AE00F38136 /* ConversationView.swift */,
|
||||||
D0C7D42324F76169001EBDBB /* CustomEmojiText.swift */,
|
D0C7D42324F76169001EBDBB /* CustomEmojiText.swift */,
|
||||||
D0BEB21024FA2A90001B0F04 /* EditFilterView.swift */,
|
D0BEB21024FA2A90001B0F04 /* EditFilterView.swift */,
|
||||||
D0BEB20424FA1107001B0F04 /* FiltersView.swift */,
|
D0BEB20424FA1107001B0F04 /* FiltersView.swift */,
|
||||||
|
@ -606,6 +618,7 @@
|
||||||
files = (
|
files = (
|
||||||
D0C7D4A324F7616A001EBDBB /* TabNavigationView.swift in Sources */,
|
D0C7D4A324F7616A001EBDBB /* TabNavigationView.swift in Sources */,
|
||||||
D02E1F95250B13210071AD56 /* SafariView.swift in Sources */,
|
D02E1F95250B13210071AD56 /* SafariView.swift in Sources */,
|
||||||
|
D00702292555E51200F38136 /* ConversationListCell.swift in Sources */,
|
||||||
D03B1B2A253818F3008F964B /* MediaPreferencesView.swift in Sources */,
|
D03B1B2A253818F3008F964B /* MediaPreferencesView.swift in Sources */,
|
||||||
D0C7D49C24F7616A001EBDBB /* RootView.swift in Sources */,
|
D0C7D49C24F7616A001EBDBB /* RootView.swift in Sources */,
|
||||||
D0F0B126251A90F400942152 /* AccountListCell.swift in Sources */,
|
D0F0B126251A90F400942152 /* AccountListCell.swift in Sources */,
|
||||||
|
@ -615,6 +628,7 @@
|
||||||
D0E569E0252931B100FA1D72 /* LoadMoreContentConfiguration.swift in Sources */,
|
D0E569E0252931B100FA1D72 /* LoadMoreContentConfiguration.swift in Sources */,
|
||||||
D0FE1C9825368A9D003EF1EB /* PlayerCache.swift in Sources */,
|
D0FE1C9825368A9D003EF1EB /* PlayerCache.swift in Sources */,
|
||||||
D0F0B136251AA12700942152 /* CollectionItem+Extensions.swift in Sources */,
|
D0F0B136251AA12700942152 /* CollectionItem+Extensions.swift in Sources */,
|
||||||
|
D007023E25562A2800F38136 /* ConversationAvatarsView.swift in Sources */,
|
||||||
D0625E5D250F0B5C00502611 /* StatusContentConfiguration.swift in Sources */,
|
D0625E5D250F0B5C00502611 /* StatusContentConfiguration.swift in Sources */,
|
||||||
D0BEB1F324F8EE8C001B0F04 /* StatusAttachmentView.swift in Sources */,
|
D0BEB1F324F8EE8C001B0F04 /* StatusAttachmentView.swift in Sources */,
|
||||||
D0C7D49A24F7616A001EBDBB /* TableView.swift in Sources */,
|
D0C7D49A24F7616A001EBDBB /* TableView.swift in Sources */,
|
||||||
|
@ -638,6 +652,7 @@
|
||||||
D08B8D8D2544E6EC00B1EBEF /* PollResultView.swift in Sources */,
|
D08B8D8D2544E6EC00B1EBEF /* PollResultView.swift in Sources */,
|
||||||
D0C7D4D524F7616A001EBDBB /* String+Extensions.swift in Sources */,
|
D0C7D4D524F7616A001EBDBB /* String+Extensions.swift in Sources */,
|
||||||
D0C7D4A224F7616A001EBDBB /* NotificationTypesPreferencesView.swift in Sources */,
|
D0C7D4A224F7616A001EBDBB /* NotificationTypesPreferencesView.swift in Sources */,
|
||||||
|
D00702362555F4C500F38136 /* ConversationContentConfiguration.swift in Sources */,
|
||||||
D0BEB1F724F9A84B001B0F04 /* LoadingTableFooterView.swift in Sources */,
|
D0BEB1F724F9A84B001B0F04 /* LoadingTableFooterView.swift in Sources */,
|
||||||
D06BC5E625202AD90079541D /* ProfileViewController.swift in Sources */,
|
D06BC5E625202AD90079541D /* ProfileViewController.swift in Sources */,
|
||||||
D01C6FAC252024BD003D0300 /* Array+Extensions.swift in Sources */,
|
D01C6FAC252024BD003D0300 /* Array+Extensions.swift in Sources */,
|
||||||
|
@ -656,6 +671,7 @@
|
||||||
D08B8D3D253F929E00B1EBEF /* ImageViewController.swift in Sources */,
|
D08B8D3D253F929E00B1EBEF /* ImageViewController.swift in Sources */,
|
||||||
D0B8510C25259E56004E0744 /* LoadMoreCell.swift in Sources */,
|
D0B8510C25259E56004E0744 /* LoadMoreCell.swift in Sources */,
|
||||||
D01F41E424F8889700D55A2D /* StatusAttachmentsView.swift in Sources */,
|
D01F41E424F8889700D55A2D /* StatusAttachmentsView.swift in Sources */,
|
||||||
|
D00702312555F4AE00F38136 /* ConversationView.swift in Sources */,
|
||||||
D0BEB21124FA2A91001B0F04 /* EditFilterView.swift in Sources */,
|
D0BEB21124FA2A91001B0F04 /* EditFilterView.swift in Sources */,
|
||||||
D08B8D4A253FC36500B1EBEF /* ImageNavigationController.swift in Sources */,
|
D08B8D4A253FC36500B1EBEF /* ImageNavigationController.swift in Sources */,
|
||||||
D0030982250C6C8500EACB32 /* URL+Extensions.swift in Sources */,
|
D0030982250C6C8500EACB32 /* URL+Extensions.swift in Sources */,
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import DB
|
||||||
|
import Foundation
|
||||||
|
import Mastodon
|
||||||
|
import MastodonAPI
|
||||||
|
|
||||||
|
public struct ConversationService {
|
||||||
|
public let conversation: Conversation
|
||||||
|
public let navigationService: NavigationService
|
||||||
|
private let mastodonAPIClient: MastodonAPIClient
|
||||||
|
private let contentDatabase: ContentDatabase
|
||||||
|
|
||||||
|
init(conversation: Conversation, mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) {
|
||||||
|
self.conversation = conversation
|
||||||
|
self.navigationService = NavigationService(
|
||||||
|
mastodonAPIClient: mastodonAPIClient,
|
||||||
|
contentDatabase: contentDatabase)
|
||||||
|
self.mastodonAPIClient = mastodonAPIClient
|
||||||
|
self.contentDatabase = contentDatabase
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,40 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import DB
|
||||||
|
import Foundation
|
||||||
|
import Mastodon
|
||||||
|
import MastodonAPI
|
||||||
|
|
||||||
|
public struct ConversationsService {
|
||||||
|
public let sections: AnyPublisher<[[CollectionItem]], Error>
|
||||||
|
public let nextPageMaxId: AnyPublisher<String, Never>
|
||||||
|
public let navigationService: NavigationService
|
||||||
|
|
||||||
|
private let mastodonAPIClient: MastodonAPIClient
|
||||||
|
private let contentDatabase: ContentDatabase
|
||||||
|
private let nextPageMaxIdSubject = PassthroughSubject<String, Never>()
|
||||||
|
|
||||||
|
init(mastodonAPIClient: MastodonAPIClient, contentDatabase: ContentDatabase) {
|
||||||
|
self.mastodonAPIClient = mastodonAPIClient
|
||||||
|
self.contentDatabase = contentDatabase
|
||||||
|
sections = contentDatabase.conversationsPublisher()
|
||||||
|
.map { [$0.map(CollectionItem.conversation)] }
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
nextPageMaxId = nextPageMaxIdSubject.eraseToAnyPublisher()
|
||||||
|
navigationService = NavigationService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ConversationsService: CollectionService {
|
||||||
|
public func request(maxId: String?, minId: String?) -> AnyPublisher<Never, Error> {
|
||||||
|
mastodonAPIClient.pagedRequest(ConversationsEndpoint.conversations, maxId: maxId, minId: minId)
|
||||||
|
.handleEvents(receiveOutput: {
|
||||||
|
guard let maxId = $0.info.maxId else { return }
|
||||||
|
|
||||||
|
nextPageMaxIdSubject.send(maxId)
|
||||||
|
})
|
||||||
|
.flatMap { contentDatabase.insert(conversations: $0.result) }
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
|
@ -212,6 +212,10 @@ public extension IdentityService {
|
||||||
func notificationsService() -> NotificationsService {
|
func notificationsService() -> NotificationsService {
|
||||||
NotificationsService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
NotificationsService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func conversationsService() -> ConversationsService {
|
||||||
|
ConversationsService(mastodonAPIClient: mastodonAPIClient, contentDatabase: contentDatabase)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension IdentityService {
|
private extension IdentityService {
|
||||||
|
|
|
@ -79,6 +79,13 @@ public extension NavigationService {
|
||||||
mastodonAPIClient: mastodonAPIClient,
|
mastodonAPIClient: mastodonAPIClient,
|
||||||
contentDatabase: contentDatabase)
|
contentDatabase: contentDatabase)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func conversationService(conversation: Conversation) -> ConversationService {
|
||||||
|
ConversationService(
|
||||||
|
conversation: conversation,
|
||||||
|
mastodonAPIClient: mastodonAPIClient,
|
||||||
|
contentDatabase: contentDatabase)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension NavigationService {
|
private extension NavigationService {
|
||||||
|
|
|
@ -140,6 +140,13 @@ extension CollectionItemsViewModel: CollectionViewModel {
|
||||||
.navigationService
|
.navigationService
|
||||||
.profileService(account: notification.account))))
|
.profileService(account: notification.account))))
|
||||||
}
|
}
|
||||||
|
case let .conversation(conversation):
|
||||||
|
guard let status = conversation.lastStatus else { break }
|
||||||
|
|
||||||
|
eventsSubject.send(
|
||||||
|
.navigation(.collection(collectionService
|
||||||
|
.navigationService
|
||||||
|
.contextService(id: status.displayStatus.id))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -164,7 +171,7 @@ extension CollectionItemsViewModel: CollectionViewModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// swiftlint:disable:next function_body_length
|
// swiftlint:disable:next function_body_length cyclomatic_complexity
|
||||||
public func viewModel(indexPath: IndexPath) -> CollectionItemViewModel {
|
public func viewModel(indexPath: IndexPath) -> CollectionItemViewModel {
|
||||||
let item = items.value[indexPath.section][indexPath.item]
|
let item = items.value[indexPath.section][indexPath.item]
|
||||||
let cachedViewModel = viewModelCache[item]?.viewModel
|
let cachedViewModel = viewModelCache[item]?.viewModel
|
||||||
|
@ -228,6 +235,19 @@ extension CollectionItemsViewModel: CollectionViewModel {
|
||||||
cache(viewModel: viewModel, forItem: item)
|
cache(viewModel: viewModel, forItem: item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return viewModel
|
||||||
|
case let .conversation(conversation):
|
||||||
|
if let cachedViewModel = cachedViewModel {
|
||||||
|
return cachedViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
let viewModel = ConversationViewModel(
|
||||||
|
conversationService: collectionService.navigationService.conversationService(
|
||||||
|
conversation: conversation),
|
||||||
|
identification: identification)
|
||||||
|
|
||||||
|
cache(viewModel: viewModel, forItem: item)
|
||||||
|
|
||||||
return viewModel
|
return viewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import Foundation
|
||||||
|
import Mastodon
|
||||||
|
import ServiceLayer
|
||||||
|
|
||||||
|
public final class ConversationViewModel: CollectionItemViewModel, ObservableObject {
|
||||||
|
public let accountViewModels: [AccountViewModel]
|
||||||
|
public let statusViewModel: StatusViewModel?
|
||||||
|
public let events: AnyPublisher<AnyPublisher<CollectionItemEvent, Error>, Never>
|
||||||
|
|
||||||
|
private let conversationService: ConversationService
|
||||||
|
private let identification: Identification
|
||||||
|
private let eventsSubject = PassthroughSubject<AnyPublisher<CollectionItemEvent, Error>, Never>()
|
||||||
|
|
||||||
|
init(conversationService: ConversationService, identification: Identification) {
|
||||||
|
accountViewModels = conversationService.conversation.accounts.map {
|
||||||
|
AccountViewModel(
|
||||||
|
accountService: conversationService.navigationService.accountService(account: $0),
|
||||||
|
identification: identification)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let status = conversationService.conversation.lastStatus {
|
||||||
|
statusViewModel = StatusViewModel(
|
||||||
|
statusService: conversationService.navigationService.statusService(status: status),
|
||||||
|
identification: identification)
|
||||||
|
} else {
|
||||||
|
statusViewModel = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
self.conversationService = conversationService
|
||||||
|
self.identification = identification
|
||||||
|
self.events = eventsSubject.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
|
@ -34,7 +34,22 @@ public final class NavigationViewModel: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var conversationsViewModel: CollectionViewModel? {
|
||||||
|
if identification.identity.authenticated {
|
||||||
|
if _conversationsViewModel == nil {
|
||||||
|
_conversationsViewModel = CollectionItemsViewModel(
|
||||||
|
collectionService: identification.service.conversationsService(),
|
||||||
|
identification: identification)
|
||||||
|
}
|
||||||
|
|
||||||
|
return _conversationsViewModel
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var _notificationsViewModel: CollectionViewModel?
|
private var _notificationsViewModel: CollectionViewModel?
|
||||||
|
private var _conversationsViewModel: CollectionViewModel?
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
public init(identification: Identification) {
|
public init(identification: Identification) {
|
||||||
|
|
|
@ -0,0 +1,88 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Kingfisher
|
||||||
|
import UIKit
|
||||||
|
import ViewModels
|
||||||
|
|
||||||
|
final class ConversationAvatarsView: UIView {
|
||||||
|
private let leftStackView = UIStackView()
|
||||||
|
private let rightStackView = UIStackView()
|
||||||
|
|
||||||
|
var viewModel: ConversationViewModel? {
|
||||||
|
didSet {
|
||||||
|
for stackView in [leftStackView, rightStackView] {
|
||||||
|
for view in stackView.arrangedSubviews {
|
||||||
|
stackView.removeArrangedSubview(view)
|
||||||
|
view.removeFromSuperview()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let accountViewModels = viewModel?.accountViewModels ?? []
|
||||||
|
let accountCount = accountViewModels.count
|
||||||
|
|
||||||
|
rightStackView.isHidden = accountCount == 1
|
||||||
|
|
||||||
|
for (index, accountViewModel) in accountViewModels.enumerated() {
|
||||||
|
let imageView = AnimatedImageView()
|
||||||
|
|
||||||
|
imageView.contentMode = .scaleAspectFill
|
||||||
|
imageView.clipsToBounds = true
|
||||||
|
imageView.kf.setImage(with: accountViewModel.avatarURL())
|
||||||
|
|
||||||
|
if accountCount == 2 && index == 1
|
||||||
|
|| accountCount == 3 && index != 0
|
||||||
|
|| accountCount > 3 && index % 2 != 0 {
|
||||||
|
rightStackView.addArrangedSubview(imageView)
|
||||||
|
} else {
|
||||||
|
leftStackView.addArrangedSubview(imageView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
initialSetup()
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
layer.cornerRadius = bounds.height / 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension ConversationAvatarsView {
|
||||||
|
func initialSetup() {
|
||||||
|
backgroundColor = .clear
|
||||||
|
clipsToBounds = true
|
||||||
|
|
||||||
|
let containerStackView = UIStackView()
|
||||||
|
|
||||||
|
addSubview(containerStackView)
|
||||||
|
containerStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
containerStackView.distribution = .fillEqually
|
||||||
|
containerStackView.spacing = .ultraCompactSpacing
|
||||||
|
leftStackView.distribution = .fillEqually
|
||||||
|
leftStackView.spacing = .ultraCompactSpacing
|
||||||
|
leftStackView.axis = .vertical
|
||||||
|
rightStackView.distribution = .fillEqually
|
||||||
|
rightStackView.spacing = .ultraCompactSpacing
|
||||||
|
rightStackView.axis = .vertical
|
||||||
|
containerStackView.addArrangedSubview(leftStackView)
|
||||||
|
containerStackView.addArrangedSubview(rightStackView)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
containerStackView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
containerStackView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
containerStackView.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
containerStackView.bottomAnchor.constraint(equalTo: bottomAnchor)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import ViewModels
|
||||||
|
|
||||||
|
struct ConversationContentConfiguration {
|
||||||
|
let viewModel: ConversationViewModel
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ConversationContentConfiguration: UIContentConfiguration {
|
||||||
|
func makeContentView() -> UIView & UIContentView {
|
||||||
|
ConversationView(configuration: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updated(for state: UIConfigurationState) -> ConversationContentConfiguration {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import ViewModels
|
||||||
|
|
||||||
|
class ConversationListCell: UITableViewCell {
|
||||||
|
var viewModel: ConversationViewModel?
|
||||||
|
|
||||||
|
override func updateConfiguration(using state: UICellConfigurationState) {
|
||||||
|
guard let viewModel = viewModel else { return }
|
||||||
|
|
||||||
|
contentConfiguration = ConversationContentConfiguration(viewModel: viewModel).updated(for: state)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
if UIDevice.current.userInterfaceIdiom == .phone {
|
||||||
|
separatorInset.left = 0
|
||||||
|
separatorInset.right = 0
|
||||||
|
} else {
|
||||||
|
separatorInset.left = layoutMargins.left
|
||||||
|
separatorInset.right = layoutMargins.right
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
final class ConversationView: UIView {
|
||||||
|
let avatarsView = ConversationAvatarsView()
|
||||||
|
let displayNamesLabel = UILabel()
|
||||||
|
let statusBodyView = StatusBodyView()
|
||||||
|
|
||||||
|
private var conversationConfiguration: ConversationContentConfiguration
|
||||||
|
|
||||||
|
init(configuration: ConversationContentConfiguration) {
|
||||||
|
conversationConfiguration = configuration
|
||||||
|
|
||||||
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
initialSetup()
|
||||||
|
applyConversationConfiguration()
|
||||||
|
}
|
||||||
|
|
||||||
|
@available(*, unavailable)
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ConversationView: UIContentView {
|
||||||
|
var configuration: UIContentConfiguration {
|
||||||
|
get { conversationConfiguration }
|
||||||
|
set {
|
||||||
|
guard let conversationConfiguration = newValue as? ConversationContentConfiguration else { return }
|
||||||
|
|
||||||
|
self.conversationConfiguration = conversationConfiguration
|
||||||
|
|
||||||
|
applyConversationConfiguration()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension ConversationView {
|
||||||
|
func initialSetup() {
|
||||||
|
let containerStackView = UIStackView()
|
||||||
|
let sideStackView = UIStackView()
|
||||||
|
let mainStackView = UIStackView()
|
||||||
|
|
||||||
|
addSubview(containerStackView)
|
||||||
|
containerStackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
containerStackView.spacing = .defaultSpacing
|
||||||
|
|
||||||
|
sideStackView.axis = .vertical
|
||||||
|
sideStackView.alignment = .trailing
|
||||||
|
sideStackView.spacing = .compactSpacing
|
||||||
|
sideStackView.addArrangedSubview(avatarsView)
|
||||||
|
sideStackView.addArrangedSubview(UIView())
|
||||||
|
containerStackView.addArrangedSubview(sideStackView)
|
||||||
|
|
||||||
|
mainStackView.axis = .vertical
|
||||||
|
mainStackView.spacing = .compactSpacing
|
||||||
|
mainStackView.addArrangedSubview(displayNamesLabel)
|
||||||
|
mainStackView.addSubview(UIView())
|
||||||
|
mainStackView.addArrangedSubview(statusBodyView)
|
||||||
|
containerStackView.addArrangedSubview(mainStackView)
|
||||||
|
|
||||||
|
displayNamesLabel.font = .preferredFont(forTextStyle: .headline)
|
||||||
|
displayNamesLabel.adjustsFontForContentSizeCategory = true
|
||||||
|
|
||||||
|
statusBodyView.alpha = 0.5
|
||||||
|
statusBodyView.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
let avatarsHeightConstraint = avatarsView.heightAnchor.constraint(equalToConstant: .avatarDimension)
|
||||||
|
|
||||||
|
avatarsHeightConstraint.priority = .justBelowMax
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
containerStackView.leadingAnchor.constraint(equalTo: readableContentGuide.leadingAnchor),
|
||||||
|
containerStackView.topAnchor.constraint(equalTo: readableContentGuide.topAnchor),
|
||||||
|
containerStackView.trailingAnchor.constraint(equalTo: readableContentGuide.trailingAnchor),
|
||||||
|
containerStackView.bottomAnchor.constraint(equalTo: readableContentGuide.bottomAnchor),
|
||||||
|
avatarsView.widthAnchor.constraint(equalToConstant: .avatarDimension),
|
||||||
|
avatarsHeightConstraint,
|
||||||
|
sideStackView.widthAnchor.constraint(equalToConstant: .avatarDimension)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyConversationConfiguration() {
|
||||||
|
let viewModel = conversationConfiguration.viewModel
|
||||||
|
let displayNames = ListFormatter.localizedString(byJoining: viewModel.accountViewModels.map(\.displayName))
|
||||||
|
let mutableDisplayNames = NSMutableAttributedString(string: displayNames)
|
||||||
|
|
||||||
|
mutableDisplayNames.insert(
|
||||||
|
emoji: viewModel.accountViewModels.map(\.emoji).reduce([], +),
|
||||||
|
view: displayNamesLabel)
|
||||||
|
mutableDisplayNames.resizeAttachments(toLineHeight: displayNamesLabel.font.lineHeight)
|
||||||
|
|
||||||
|
displayNamesLabel.attributedText = mutableDisplayNames
|
||||||
|
statusBodyView.viewModel = viewModel.statusViewModel
|
||||||
|
avatarsView.viewModel = viewModel
|
||||||
|
}
|
||||||
|
}
|
|
@ -58,6 +58,7 @@ private extension TabNavigationView {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
|
// swiftlint:disable:next function_body_length
|
||||||
func view(tab: NavigationViewModel.Tab) -> some View {
|
func view(tab: NavigationViewModel.Tab) -> some View {
|
||||||
switch tab {
|
switch tab {
|
||||||
case .timelines:
|
case .timelines:
|
||||||
|
@ -101,6 +102,15 @@ private extension TabNavigationView {
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.navigationBarItems(leading: secondaryNavigationButton)
|
.navigationBarItems(leading: secondaryNavigationButton)
|
||||||
}
|
}
|
||||||
|
case .messages:
|
||||||
|
if let conversationsViewModel = viewModel.conversationsViewModel {
|
||||||
|
TableView(viewModel: conversationsViewModel)
|
||||||
|
.id(tab)
|
||||||
|
.edgesIgnoringSafeArea(.all)
|
||||||
|
.navigationTitle("messages")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.navigationBarItems(leading: secondaryNavigationButton)
|
||||||
|
}
|
||||||
default: Text(tab.title)
|
default: Text(tab.title)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import SwiftUI
|
||||||
extension CGFloat {
|
extension CGFloat {
|
||||||
static let defaultSpacing: Self = 8
|
static let defaultSpacing: Self = 8
|
||||||
static let compactSpacing: Self = 4
|
static let compactSpacing: Self = 4
|
||||||
|
static let ultraCompactSpacing: Self = 1
|
||||||
static let defaultCornerRadius: Self = 8
|
static let defaultCornerRadius: Self = 8
|
||||||
static let avatarDimension: Self = 50
|
static let avatarDimension: Self = 50
|
||||||
static let hairline = 1 / UIScreen.main.scale
|
static let hairline = 1 / UIScreen.main.scale
|
||||||
|
|
Loading…
Reference in New Issue