mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2025-01-03 04:48:50 +00:00
New direct messages view close #122
This commit is contained in:
parent
15d3bb7177
commit
d6aa99eb57
16 changed files with 397 additions and 14 deletions
|
@ -6,6 +6,7 @@ import Lists
|
||||||
import Status
|
import Status
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Timeline
|
import Timeline
|
||||||
|
import Conversations
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
extension View {
|
extension View {
|
||||||
|
@ -18,6 +19,8 @@ extension View {
|
||||||
AccountDetailView(account: account)
|
AccountDetailView(account: account)
|
||||||
case let .statusDetail(id):
|
case let .statusDetail(id):
|
||||||
StatusDetailView(statusId: id)
|
StatusDetailView(statusId: id)
|
||||||
|
case let .conversationDetail(conversation):
|
||||||
|
ConversationDetailView(conversation: conversation)
|
||||||
case let .remoteStatusDetail(url):
|
case let .remoteStatusDetail(url):
|
||||||
StatusDetailView(remoteStatusURL: url)
|
StatusDetailView(remoteStatusURL: url)
|
||||||
case let .hashTag(tag, accountId):
|
case let .hashTag(tag, accountId):
|
||||||
|
|
|
@ -0,0 +1,139 @@
|
||||||
|
import SwiftUI
|
||||||
|
import Models
|
||||||
|
import DesignSystem
|
||||||
|
import Network
|
||||||
|
import Env
|
||||||
|
import NukeUI
|
||||||
|
|
||||||
|
public struct ConversationDetailView: View {
|
||||||
|
private enum Constants {
|
||||||
|
static let bottomAnchor = "bottom"
|
||||||
|
}
|
||||||
|
|
||||||
|
@EnvironmentObject private var quickLook: QuickLook
|
||||||
|
@EnvironmentObject private var routerPath: RouterPath
|
||||||
|
@EnvironmentObject private var currentAccount: CurrentAccount
|
||||||
|
@EnvironmentObject private var client: Client
|
||||||
|
@EnvironmentObject private var theme: Theme
|
||||||
|
@EnvironmentObject private var watcher: StreamWatcher
|
||||||
|
|
||||||
|
@StateObject private var viewModel: ConversationDetailViewModel
|
||||||
|
|
||||||
|
@FocusState private var isMessageFieldFocused: Bool
|
||||||
|
|
||||||
|
@State private var scrollProxy: ScrollViewProxy?
|
||||||
|
@State private var didAppear: Bool = false
|
||||||
|
|
||||||
|
public init(conversation: Conversation) {
|
||||||
|
_viewModel = StateObject(wrappedValue: .init(conversation: conversation))
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
ScrollViewReader { proxy in
|
||||||
|
ZStack(alignment: .bottom) {
|
||||||
|
ScrollView {
|
||||||
|
LazyVStack {
|
||||||
|
if viewModel.isLoadingMessages {
|
||||||
|
loadingView
|
||||||
|
}
|
||||||
|
ForEach(viewModel.messages) { message in
|
||||||
|
ConversationMessageView(message: message,
|
||||||
|
conversation: viewModel.conversation)
|
||||||
|
.id(message.id)
|
||||||
|
}
|
||||||
|
bottomAnchorView
|
||||||
|
}
|
||||||
|
.padding(.horizontal, .layoutPadding)
|
||||||
|
.padding(.bottom, 30)
|
||||||
|
}
|
||||||
|
.scrollDismissesKeyboard(.interactively)
|
||||||
|
inputTextView
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
scrollProxy = proxy
|
||||||
|
viewModel.client = client
|
||||||
|
isMessageFieldFocused = true
|
||||||
|
if !didAppear {
|
||||||
|
didAppear = true
|
||||||
|
Task {
|
||||||
|
await viewModel.fetchMessages()
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
withAnimation {
|
||||||
|
proxy.scrollTo(Constants.bottomAnchor, anchor: .bottom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.scrollContentBackground(.hidden)
|
||||||
|
.background(theme.primaryBackgroundColor)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .principal) {
|
||||||
|
if viewModel.conversation.accounts.count == 1,
|
||||||
|
let account = viewModel.conversation.accounts.first {
|
||||||
|
EmojiTextApp(.init(stringValue: account.safeDisplayName), emojis: account.emojis)
|
||||||
|
.font(.scaledHeadline)
|
||||||
|
} else {
|
||||||
|
Text("Direct message with \(viewModel.conversation.accounts.count) people")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: watcher.latestEvent?.id) { _ in
|
||||||
|
if let latestEvent = watcher.latestEvent {
|
||||||
|
viewModel.handleEvent(event: latestEvent)
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
withAnimation {
|
||||||
|
scrollProxy?.scrollTo(Constants.bottomAnchor, anchor: .bottom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var loadingView: some View {
|
||||||
|
ForEach(Status.placeholders()) { message in
|
||||||
|
ConversationMessageView(message: message, conversation: viewModel.conversation)
|
||||||
|
.redacted(reason: .placeholder)
|
||||||
|
.shimmering()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var bottomAnchorView: some View {
|
||||||
|
Rectangle()
|
||||||
|
.fill(Color.clear)
|
||||||
|
.frame(height: 40)
|
||||||
|
.id(Constants.bottomAnchor)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var inputTextView: some View {
|
||||||
|
VStack{
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Button {
|
||||||
|
routerPath.presentedSheet = .replyToStatusEditor(status: viewModel.conversation.lastStatus)
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "plus")
|
||||||
|
}
|
||||||
|
TextField("New messge", text: $viewModel.newMessageText, axis: .horizontal)
|
||||||
|
.textFieldStyle(.roundedBorder)
|
||||||
|
.focused($isMessageFieldFocused)
|
||||||
|
if !viewModel.newMessageText.isEmpty {
|
||||||
|
Button {
|
||||||
|
Task {
|
||||||
|
await viewModel.postMessage()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
if viewModel.isSendingMessage {
|
||||||
|
ProgressView()
|
||||||
|
} else {
|
||||||
|
Image(systemName: "paperplane")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(8)
|
||||||
|
}
|
||||||
|
.background(.thinMaterial)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
import Foundation
|
||||||
|
import Models
|
||||||
|
import Network
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
class ConversationDetailViewModel: ObservableObject {
|
||||||
|
var client: Client?
|
||||||
|
|
||||||
|
var conversation: Conversation
|
||||||
|
|
||||||
|
@Published var isLoadingMessages: Bool = true
|
||||||
|
@Published var messages: [Status] = []
|
||||||
|
|
||||||
|
@Published var isSendingMessage: Bool = false
|
||||||
|
@Published var newMessageText: String = ""
|
||||||
|
|
||||||
|
init(conversation: Conversation) {
|
||||||
|
self.conversation = conversation
|
||||||
|
messages = [conversation.lastStatus]
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchMessages() async {
|
||||||
|
guard let client, let lastMessageId = messages.last?.id else { return }
|
||||||
|
do {
|
||||||
|
let context: StatusContext = try await client.get(endpoint: Statuses.context(id: lastMessageId))
|
||||||
|
isLoadingMessages = false
|
||||||
|
messages.insert(contentsOf: context.ancestors, at: 0)
|
||||||
|
messages.append(contentsOf: context.descendants)
|
||||||
|
} catch {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func postMessage() async {
|
||||||
|
guard let client else { return }
|
||||||
|
isSendingMessage = true
|
||||||
|
var finalText = conversation.accounts.map{ "@\($0.acct)" }.joined(separator: " ")
|
||||||
|
finalText += " "
|
||||||
|
finalText += newMessageText
|
||||||
|
let data = StatusData(status: finalText,
|
||||||
|
visibility: .direct,
|
||||||
|
inReplyToId: messages.last?.id)
|
||||||
|
do {
|
||||||
|
let status: Status = try await client.post(endpoint: Statuses.postStatus(json: data))
|
||||||
|
appendNewStatus(status: status)
|
||||||
|
withAnimation {
|
||||||
|
newMessageText = ""
|
||||||
|
isSendingMessage = false
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
isSendingMessage = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleEvent(event: any StreamEvent) {
|
||||||
|
if let event = event as? StreamEventStatusUpdate,
|
||||||
|
let index = messages.firstIndex(where: { $0.id == event.status.id }) {
|
||||||
|
messages[index] = event.status
|
||||||
|
} else if let event = event as? StreamEventDelete,
|
||||||
|
let index = messages.firstIndex(where: { $0.id == event.status }) {
|
||||||
|
messages.remove(at: index)
|
||||||
|
} else if let event = event as? StreamEventConversation,
|
||||||
|
event.conversation.id == conversation.id {
|
||||||
|
self.conversation = event.conversation
|
||||||
|
appendNewStatus(status: conversation.lastStatus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func appendNewStatus(status: Status) {
|
||||||
|
if !messages.contains(where: { $0.id == status.id }) {
|
||||||
|
messages.append(status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,156 @@
|
||||||
|
import SwiftUI
|
||||||
|
import Env
|
||||||
|
import DesignSystem
|
||||||
|
import Network
|
||||||
|
import Models
|
||||||
|
import NukeUI
|
||||||
|
|
||||||
|
struct ConversationMessageView: View {
|
||||||
|
@EnvironmentObject private var quickLook: QuickLook
|
||||||
|
@EnvironmentObject private var routerPath: RouterPath
|
||||||
|
@EnvironmentObject private var currentAccount: CurrentAccount
|
||||||
|
@EnvironmentObject private var client: Client
|
||||||
|
@EnvironmentObject private var theme: Theme
|
||||||
|
|
||||||
|
let message: Status
|
||||||
|
let conversation: Conversation
|
||||||
|
|
||||||
|
@State private var isLiked: Bool = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
let isOwnMessage = message.account.id == currentAccount.account?.id
|
||||||
|
VStack {
|
||||||
|
HStack(alignment: .bottom) {
|
||||||
|
if isOwnMessage {
|
||||||
|
Spacer()
|
||||||
|
} else {
|
||||||
|
AvatarView(url: message.account.avatar, size: .status)
|
||||||
|
.onTapGesture {
|
||||||
|
routerPath.navigate(to: .accountDetailWithAccount(account: message.account))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
EmojiTextApp(message.content, emojis: message.emojis)
|
||||||
|
.font(.scaledBody)
|
||||||
|
.padding(6)
|
||||||
|
}
|
||||||
|
.background(isOwnMessage ? theme.tintColor.opacity(0.2) : theme.secondaryBackgroundColor)
|
||||||
|
.cornerRadius(8)
|
||||||
|
.padding(.leading, isOwnMessage ? 24 : 0)
|
||||||
|
.padding(.trailing, isOwnMessage ? 0 : 24)
|
||||||
|
.overlay {
|
||||||
|
if isLiked, message.account.id != currentAccount.account?.id {
|
||||||
|
likeView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.contextMenu {
|
||||||
|
contextMenu
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isOwnMessage {
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ForEach(message.mediaAttachments) { media in
|
||||||
|
makeMediaView(media)
|
||||||
|
.padding(.leading, isOwnMessage ? 24 : 0)
|
||||||
|
.padding(.trailing, isOwnMessage ? 0 : 24)
|
||||||
|
}
|
||||||
|
|
||||||
|
if message.id == conversation.lastStatus.id {
|
||||||
|
HStack {
|
||||||
|
if isOwnMessage {
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
Text(message.createdAt.asDate, style: .time)
|
||||||
|
.font(.scaledFootnote)
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
if !isOwnMessage {
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
isLiked = message.favourited == true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var contextMenu: some View {
|
||||||
|
Button {
|
||||||
|
routerPath.navigate(to: .statusDetail(id: message.id))
|
||||||
|
} label: {
|
||||||
|
Label("View detail", systemImage: "arrow.forward")
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
UIPasteboard.general.string = message.content.asRawText
|
||||||
|
} label: {
|
||||||
|
Label("status.action.copy-text", systemImage: "doc.on.doc")
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
Task {
|
||||||
|
do {
|
||||||
|
let status: Status
|
||||||
|
if isLiked {
|
||||||
|
status = try await client.post(endpoint: Statuses.unfavourite(id: message.id))
|
||||||
|
} else {
|
||||||
|
status = try await client.post(endpoint: Statuses.favourite(id: message.id))
|
||||||
|
}
|
||||||
|
withAnimation {
|
||||||
|
isLiked = status.favourited == true
|
||||||
|
}
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Label(isLiked ? "status.action.unfavorite" : "status.action.favorite",
|
||||||
|
systemImage: isLiked ? "star.fill" : "star")
|
||||||
|
}
|
||||||
|
Divider()
|
||||||
|
if message.account.id == currentAccount.account?.id {
|
||||||
|
Button("status.action.delete", role: .destructive) {
|
||||||
|
Task {
|
||||||
|
_ = try await client.delete(endpoint: Statuses.status(id: message.id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeMediaView(_ attachement: MediaAttachment) -> some View {
|
||||||
|
LazyImage(url: attachement.url) { state in
|
||||||
|
if let image = state.image {
|
||||||
|
image
|
||||||
|
.resizingMode(.aspectFill)
|
||||||
|
.cornerRadius(8)
|
||||||
|
.padding(8)
|
||||||
|
} else if state.isLoading {
|
||||||
|
RoundedRectangle(cornerRadius: 8)
|
||||||
|
.fill(Color.gray)
|
||||||
|
.frame(height: 200)
|
||||||
|
.shimmering()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(height: 200)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.onTapGesture {
|
||||||
|
if let url = attachement.url {
|
||||||
|
Task {
|
||||||
|
await quickLook.prepareFor(urls: [url], selectedURL: url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var likeView: some View {
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
VStack {
|
||||||
|
Image(systemName: "star.fill")
|
||||||
|
.foregroundColor(.yellow)
|
||||||
|
.offset(x: -16, y: -7)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -44,7 +44,7 @@ struct ConversationsListRow: View {
|
||||||
Task {
|
Task {
|
||||||
await viewModel.markAsRead(conversation: conversation)
|
await viewModel.markAsRead(conversation: conversation)
|
||||||
}
|
}
|
||||||
routerPath.navigate(to: .statusDetail(id: conversation.lastStatus.id))
|
routerPath.navigate(to: .conversationDetail(conversation: conversation))
|
||||||
}
|
}
|
||||||
.padding(.top, 4)
|
.padding(.top, 4)
|
||||||
actionsView
|
actionsView
|
||||||
|
|
|
@ -7,6 +7,7 @@ public enum RouterDestinations: Hashable {
|
||||||
case accountDetail(id: String)
|
case accountDetail(id: String)
|
||||||
case accountDetailWithAccount(account: Account)
|
case accountDetailWithAccount(account: Account)
|
||||||
case statusDetail(id: String)
|
case statusDetail(id: String)
|
||||||
|
case conversationDetail(conversation: Conversation)
|
||||||
case remoteStatusDetail(url: URL)
|
case remoteStatusDetail(url: URL)
|
||||||
case hashTag(tag: String, account: String?)
|
case hashTag(tag: String, account: String?)
|
||||||
case list(list: Models.List)
|
case list(list: Models.List)
|
||||||
|
|
|
@ -3,7 +3,7 @@ import HTML2Markdown
|
||||||
import SwiftSoup
|
import SwiftSoup
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
public struct HTMLString: Decodable, Equatable {
|
public struct HTMLString: Decodable, Equatable, Hashable {
|
||||||
public let htmlValue: String
|
public let htmlValue: String
|
||||||
public let asMarkdown: String
|
public let asMarkdown: String
|
||||||
public let asRawText: String
|
public let asRawText: String
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct Card: Codable, Identifiable {
|
public struct Card: Codable, Identifiable, Equatable, Hashable {
|
||||||
public var id: String {
|
public var id: String {
|
||||||
url
|
url
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct Conversation: Identifiable, Decodable {
|
public struct Conversation: Identifiable, Decodable, Hashable, Equatable {
|
||||||
public let id: String
|
public let id: String
|
||||||
public let unread: Bool
|
public let unread: Bool
|
||||||
public let lastStatus: Status
|
public let lastStatus: Status
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct Emoji: Codable, Hashable, Identifiable {
|
public struct Emoji: Codable, Hashable, Identifiable, Equatable {
|
||||||
public func hash(into hasher: inout Hasher) {
|
public func hash(into hasher: inout Hasher) {
|
||||||
hasher.combine(shortcode)
|
hasher.combine(shortcode)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct Filtered: Codable {
|
public struct Filtered: Codable, Equatable, Hashable {
|
||||||
public let filter: Filter
|
public let filter: Filter
|
||||||
public let keywordMatches: [String]?
|
public let keywordMatches: [String]?
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct Filter: Codable, Identifiable {
|
public struct Filter: Codable, Identifiable, Equatable, Hashable {
|
||||||
public enum Action: String, Codable {
|
public enum Action: String, Codable {
|
||||||
case warn, hide
|
case warn, hide
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct MediaAttachment: Codable, Identifiable, Hashable {
|
public struct MediaAttachment: Codable, Identifiable, Hashable, Equatable {
|
||||||
public struct MetaContainer: Codable, Equatable {
|
public struct MetaContainer: Codable, Equatable {
|
||||||
public struct Meta: Codable, Equatable {
|
public struct Meta: Codable, Equatable {
|
||||||
public let width: Int?
|
public let width: Int?
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct Mention: Codable {
|
public struct Mention: Codable, Equatable, Hashable {
|
||||||
public let id: String
|
public let id: String
|
||||||
public let username: String
|
public let username: String
|
||||||
public let url: URL
|
public let url: URL
|
||||||
|
|
|
@ -1,6 +1,14 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct Poll: Codable {
|
public struct Poll: Codable, Equatable, Hashable {
|
||||||
|
public static func == (lhs: Poll, rhs: Poll) -> Bool {
|
||||||
|
lhs.id == rhs.id
|
||||||
|
}
|
||||||
|
|
||||||
|
public func hash(into hasher: inout Hasher) {
|
||||||
|
hasher.combine(id)
|
||||||
|
}
|
||||||
|
|
||||||
public struct Option: Identifiable, Codable {
|
public struct Option: Identifiable, Codable {
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case title, votesCount
|
case title, votesCount
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct Application: Codable, Identifiable {
|
public struct Application: Codable, Identifiable, Hashable, Equatable {
|
||||||
public var id: String {
|
public var id: String {
|
||||||
name
|
name
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ public extension Application {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Visibility: String, Codable, CaseIterable {
|
public enum Visibility: String, Codable, CaseIterable, Hashable, Equatable {
|
||||||
case pub = "public"
|
case pub = "public"
|
||||||
case unlisted
|
case unlisted
|
||||||
case priv = "private"
|
case priv = "private"
|
||||||
|
@ -54,7 +54,7 @@ public protocol AnyStatus {
|
||||||
var language: String? { get }
|
var language: String? { get }
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct Status: AnyStatus, Decodable, Identifiable {
|
public struct Status: AnyStatus, Decodable, Identifiable, Equatable, Hashable {
|
||||||
public var viewId: String {
|
public var viewId: String {
|
||||||
id + createdAt + (editedAt ?? "")
|
id + createdAt + (editedAt ?? "")
|
||||||
}
|
}
|
||||||
|
@ -120,7 +120,7 @@ public struct Status: AnyStatus, Decodable, Identifiable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct ReblogStatus: AnyStatus, Decodable, Identifiable {
|
public struct ReblogStatus: AnyStatus, Decodable, Identifiable, Equatable, Hashable {
|
||||||
public var viewId: String {
|
public var viewId: String {
|
||||||
id + createdAt + (editedAt ?? "")
|
id + createdAt + (editedAt ?? "")
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,7 @@ public struct StatusEditorView: View {
|
||||||
TextView($viewModel.statusText, $viewModel.selectedRange)
|
TextView($viewModel.statusText, $viewModel.selectedRange)
|
||||||
.placeholder(String(localized: "status.editor.text.placeholder"))
|
.placeholder(String(localized: "status.editor.text.placeholder"))
|
||||||
.font(Font.scaledBodyUIFont)
|
.font(Font.scaledBodyUIFont)
|
||||||
|
.keyboardType(.twitter)
|
||||||
.padding(.horizontal, .layoutPadding)
|
.padding(.horizontal, .layoutPadding)
|
||||||
StatusEditorMediaView(viewModel: viewModel)
|
StatusEditorMediaView(viewModel: viewModel)
|
||||||
if let status = viewModel.embeddedStatus {
|
if let status = viewModel.embeddedStatus {
|
||||||
|
|
Loading…
Reference in a new issue