mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-11-22 08:20:59 +00:00
* Support for follow requests (#321) * Run SwiftFormat Co-authored-by: Thomas Ricouard <ricouard77@gmail.com>
This commit is contained in:
parent
3ccd66a6bb
commit
9b3b3692ee
42 changed files with 371 additions and 170 deletions
|
@ -1,12 +1,12 @@
|
|||
import Account
|
||||
import AppAccount
|
||||
import Conversations
|
||||
import DesignSystem
|
||||
import Env
|
||||
import Lists
|
||||
import Status
|
||||
import SwiftUI
|
||||
import Timeline
|
||||
import Conversations
|
||||
|
||||
@MainActor
|
||||
extension View {
|
||||
|
|
|
@ -214,17 +214,17 @@ class AppDelegate: NSObject, UIApplicationDelegate {
|
|||
didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data)
|
||||
{
|
||||
PushNotificationsService.shared.pushToken = deviceToken
|
||||
#if !DEBUG
|
||||
Task {
|
||||
await PushNotificationsService.shared.fetchSubscriptions(accounts: AppAccountsManager.shared.pushAccounts)
|
||||
await PushNotificationsService.shared.updateSubscriptions(accounts: AppAccountsManager.shared.pushAccounts)
|
||||
}
|
||||
#endif
|
||||
#if !DEBUG
|
||||
Task {
|
||||
await PushNotificationsService.shared.fetchSubscriptions(accounts: AppAccountsManager.shared.pushAccounts)
|
||||
await PushNotificationsService.shared.updateSubscriptions(accounts: AppAccountsManager.shared.pushAccounts)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
func application(_: UIApplication, didFailToRegisterForRemoteNotificationsWithError _: Error) {}
|
||||
|
||||
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
|
||||
func application(_: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options _: UIScene.ConnectionOptions) -> UISceneConfiguration {
|
||||
let configuration = UISceneConfiguration(name: nil, sessionRole: connectingSceneSession.role)
|
||||
if connectingSceneSession.role == .windowApplication {
|
||||
configuration.delegateClass = SceneDelegate.self
|
||||
|
|
|
@ -51,12 +51,12 @@ struct QuickLookPreview: UIViewControllerRepresentable {
|
|||
|
||||
class AppQLPreviewController: QLPreviewController {
|
||||
private var closeButton: UIBarButtonItem {
|
||||
.init(
|
||||
title: NSLocalizedString("action.done", comment: ""),
|
||||
style: .plain,
|
||||
target: self,
|
||||
action: #selector(onCloseButton)
|
||||
)
|
||||
.init(
|
||||
title: NSLocalizedString("action.done", comment: ""),
|
||||
style: .plain,
|
||||
target: self,
|
||||
action: #selector(onCloseButton)
|
||||
)
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
|
|
|
@ -166,6 +166,10 @@
|
|||
"account.follow.follow" = "Folgen";
|
||||
"account.follow.following" = "Gefolgt";
|
||||
"account.follow.requested" = "Angefragt";
|
||||
"account.follow-request.accept" = "Accept";
|
||||
"account.follow-request.reject" = "Reject";
|
||||
"account.follow-requests.pending-requests" = "Pending requests";
|
||||
"account.follow-requests.instructions" = "Those users won't see your posts until you accept them.";
|
||||
"account.followers" = "Follower";
|
||||
"account.following" = "Folgt";
|
||||
"account.list.create" = "Neue Liste erstellen";
|
||||
|
|
|
@ -169,6 +169,10 @@
|
|||
"account.follow.follow" = "Follow";
|
||||
"account.follow.following" = "Following";
|
||||
"account.follow.requested" = "Requested";
|
||||
"account.follow-request.accept" = "Accept";
|
||||
"account.follow-request.reject" = "Reject";
|
||||
"account.follow-requests.pending-requests" = "Pending requests";
|
||||
"account.follow-requests.instructions" = "Those users won't see your posts until you accept them.";
|
||||
"account.followers" = "Followers";
|
||||
"account.following" = "Following";
|
||||
"account.list.create" = "Create a new list";
|
||||
|
|
|
@ -166,6 +166,10 @@
|
|||
"account.follow.follow" = "Seguir";
|
||||
"account.follow.following" = "Siguiendo";
|
||||
"account.follow.requested" = "Solicitado";
|
||||
"account.follow-request.accept" = "Accept";
|
||||
"account.follow-request.reject" = "Reject";
|
||||
"account.follow-requests.pending-requests" = "Pending requests";
|
||||
"account.follow-requests.instructions" = "Those users won't see your posts until you accept them.";
|
||||
"account.followers" = "Seguidores";
|
||||
"account.following" = "Siguiendo";
|
||||
"account.list.create" = "Crear una lista nueva";
|
||||
|
|
|
@ -166,6 +166,10 @@
|
|||
"account.follow.follow" = "Segui";
|
||||
"account.follow.following" = "Segui già";
|
||||
"account.follow.requested" = "Richiesto";
|
||||
"account.follow-request.accept" = "Accept";
|
||||
"account.follow-request.reject" = "Reject";
|
||||
"account.follow-requests.pending-requests" = "Pending requests";
|
||||
"account.follow-requests.instructions" = "Those users won't see your posts until you accept them.";
|
||||
"account.followers" = "Seguito da";
|
||||
"account.following" = "Seguiti";
|
||||
"account.list.create" = "Crea una nuova lista";
|
||||
|
|
|
@ -152,6 +152,10 @@
|
|||
"account.follow.follow" = "フォロー";
|
||||
"account.follow.following" = "フォローしている";
|
||||
"account.follow.requested" = "リクエストしました";
|
||||
"account.follow-request.accept" = "Accept";
|
||||
"account.follow-request.reject" = "Reject";
|
||||
"account.follow-requests.pending-requests" = "Pending requests";
|
||||
"account.follow-requests.instructions" = "Those users won't see your posts until you accept them.";
|
||||
"account.followers" = "フォロワー";
|
||||
"account.following" = "フォローしている";
|
||||
"account.list.create" = "新しいリストを作成";
|
||||
|
|
|
@ -166,6 +166,10 @@
|
|||
"account.follow.follow" = "Volg";
|
||||
"account.follow.following" = "Volgend";
|
||||
"account.follow.requested" = "Verzocht";
|
||||
"account.follow-request.accept" = "Accept";
|
||||
"account.follow-request.reject" = "Reject";
|
||||
"account.follow-requests.pending-requests" = "Pending requests";
|
||||
"account.follow-requests.instructions" = "Those users won't see your posts until you accept them.";
|
||||
"account.followers" = "Volgers";
|
||||
"account.following" = "Volgend";
|
||||
"account.list.create" = "Maak een nieuwe lijst";
|
||||
|
|
|
@ -167,6 +167,10 @@
|
|||
"account.follow.follow" = "关注";
|
||||
"account.follow.following" = "正在关注";
|
||||
"account.follow.requested" = "已申请";
|
||||
"account.follow-request.accept" = "Accept";
|
||||
"account.follow-request.reject" = "Reject";
|
||||
"account.follow-requests.pending-requests" = "Pending requests";
|
||||
"account.follow-requests.instructions" = "Those users won't see your posts until you accept them.";
|
||||
"account.followers" = "粉丝";
|
||||
"account.following" = "关注";
|
||||
"account.list.create" = "新建一个列表";
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import AppAccount
|
||||
import CryptoKit
|
||||
import Env
|
||||
import KeychainSwift
|
||||
import Models
|
||||
import UIKit
|
||||
import UserNotifications
|
||||
import AppAccount
|
||||
|
||||
@MainActor
|
||||
class NotificationService: UNNotificationServiceExtension {
|
||||
|
|
|
@ -10,6 +10,7 @@ struct AccountDetailHeaderView: View {
|
|||
@EnvironmentObject private var theme: Theme
|
||||
@EnvironmentObject private var quickLook: QuickLook
|
||||
@EnvironmentObject private var routerPath: RouterPath
|
||||
@EnvironmentObject private var currentAccount: CurrentAccount
|
||||
@Environment(\.redactionReasons) private var reasons
|
||||
|
||||
@ObservedObject var viewModel: AccountDetailViewModel
|
||||
|
@ -95,7 +96,11 @@ struct AccountDetailHeaderView: View {
|
|||
makeCustomInfoLabel(title: "account.following", count: account.followingCount)
|
||||
}
|
||||
NavigationLink(value: RouterDestinations.followers(id: account.id)) {
|
||||
makeCustomInfoLabel(title: "account.followers", count: account.followersCount)
|
||||
makeCustomInfoLabel(
|
||||
title: "account.followers",
|
||||
count: account.followersCount,
|
||||
needsBadge: currentAccount.account?.id == account.id && !currentAccount.followRequests.isEmpty
|
||||
)
|
||||
}
|
||||
}.offset(y: 20)
|
||||
}
|
||||
|
@ -136,11 +141,19 @@ struct AccountDetailHeaderView: View {
|
|||
.offset(y: -40)
|
||||
}
|
||||
|
||||
private func makeCustomInfoLabel(title: LocalizedStringKey, count: Int) -> some View {
|
||||
private func makeCustomInfoLabel(title: LocalizedStringKey, count: Int, needsBadge: Bool = false) -> some View {
|
||||
VStack {
|
||||
Text("\(count)")
|
||||
.font(.scaledHeadline)
|
||||
.foregroundColor(theme.tintColor)
|
||||
.overlay(alignment: .trailing) {
|
||||
if needsBadge {
|
||||
Circle()
|
||||
.fill(Color.red)
|
||||
.frame(width: 9, height: 9)
|
||||
.offset(x: 12)
|
||||
}
|
||||
}
|
||||
Text(title)
|
||||
.font(.scaledFootnote)
|
||||
.foregroundColor(.gray)
|
||||
|
|
|
@ -10,9 +10,9 @@ public class AccountsListRowViewModel: ObservableObject {
|
|||
var client: Client?
|
||||
|
||||
@Published var account: Account
|
||||
@Published var relationShip: Relationship
|
||||
@Published var relationShip: Relationship?
|
||||
|
||||
public init(account: Account, relationShip: Relationship) {
|
||||
public init(account: Account, relationShip: Relationship? = nil) {
|
||||
self.account = account
|
||||
self.relationShip = relationShip
|
||||
}
|
||||
|
@ -24,9 +24,13 @@ public struct AccountsListRow: View {
|
|||
@EnvironmentObject private var client: Client
|
||||
|
||||
@StateObject var viewModel: AccountsListRowViewModel
|
||||
let isFollowRequest: Bool
|
||||
let requestUpdated: (() -> Void)?
|
||||
|
||||
public init(viewModel: AccountsListRowViewModel) {
|
||||
public init(viewModel: AccountsListRowViewModel, isFollowRequest: Bool = false, requestUpdated: (() -> Void)? = nil) {
|
||||
_viewModel = StateObject(wrappedValue: viewModel)
|
||||
self.isFollowRequest = isFollowRequest
|
||||
self.requestUpdated = requestUpdated
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
|
@ -45,11 +49,17 @@ public struct AccountsListRow: View {
|
|||
.environment(\.openURL, OpenURLAction { url in
|
||||
routerPath.handle(url: url)
|
||||
})
|
||||
if isFollowRequest {
|
||||
FollowRequestButtons(account: viewModel.account,
|
||||
requestUpdated: requestUpdated)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
if currentAccount.account?.id != viewModel.account.id {
|
||||
if currentAccount.account?.id != viewModel.account.id,
|
||||
let relationShip = viewModel.relationShip
|
||||
{
|
||||
FollowButton(viewModel: .init(accountId: viewModel.account.id,
|
||||
relationship: viewModel.relationShip,
|
||||
relationship: relationShip,
|
||||
shouldDisplayNotify: false,
|
||||
relationshipUpdated: { _ in }))
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import SwiftUI
|
|||
public struct AccountsListView: View {
|
||||
@EnvironmentObject private var theme: Theme
|
||||
@EnvironmentObject private var client: Client
|
||||
@EnvironmentObject private var currentAccount: CurrentAccount
|
||||
@StateObject private var viewModel: AccountsListViewModel
|
||||
@State private var didAppear: Bool = false
|
||||
|
||||
|
@ -26,11 +27,37 @@ public struct AccountsListView: View {
|
|||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
}
|
||||
case let .display(accounts, relationships, nextPageState):
|
||||
ForEach(accounts) { account in
|
||||
if let relationship = relationships.first(where: { $0.id == account.id }) {
|
||||
AccountsListRow(viewModel: .init(account: account,
|
||||
relationShip: relationship))
|
||||
if case .followers = viewModel.mode,
|
||||
!currentAccount.followRequests.isEmpty
|
||||
{
|
||||
Section(
|
||||
header: Text("account.follow-requests.pending-requests"),
|
||||
footer: Text("account.follow-requests.instructions")
|
||||
.font(.scaledFootnote)
|
||||
.foregroundColor(.secondary)
|
||||
.offset(y: -8)
|
||||
) {
|
||||
ForEach(currentAccount.followRequests) { account in
|
||||
AccountsListRow(
|
||||
viewModel: .init(account: account),
|
||||
isFollowRequest: true,
|
||||
requestUpdated: {
|
||||
Task {
|
||||
await viewModel.fetch()
|
||||
}
|
||||
}
|
||||
)
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
Section {
|
||||
ForEach(accounts) { account in
|
||||
if let relationship = relationships.first(where: { $0.id == account.id }) {
|
||||
AccountsListRow(viewModel: .init(account: account,
|
||||
relationShip: relationship))
|
||||
.listRowBackground(theme.primaryBackgroundColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -66,7 +66,7 @@ class AccountsListViewModel: ObservableObject {
|
|||
maxId: nil))
|
||||
case let .favoritedBy(statusId):
|
||||
(accounts, link) = try await client.getWithLink(endpoint: Statuses.favoritedBy(id: statusId,
|
||||
maxId: nil))
|
||||
maxId: nil))
|
||||
}
|
||||
nextPageId = link?.maxId
|
||||
relationships = try await client.get(endpoint:
|
||||
|
@ -95,7 +95,7 @@ class AccountsListViewModel: ObservableObject {
|
|||
maxId: nextPageId))
|
||||
case let .favoritedBy(statusId):
|
||||
(newAccounts, link) = try await client.getWithLink(endpoint: Statuses.favoritedBy(id: statusId,
|
||||
maxId: nextPageId))
|
||||
maxId: nextPageId))
|
||||
}
|
||||
accounts.append(contentsOf: newAccounts)
|
||||
let newRelationships: [Relationship] =
|
||||
|
|
|
@ -31,7 +31,8 @@ public struct AppAccount: Codable, Identifiable {
|
|||
|
||||
public init(server: String,
|
||||
accountName: String?,
|
||||
oauthToken: OauthToken? = nil) {
|
||||
oauthToken: OauthToken? = nil)
|
||||
{
|
||||
self.server = server
|
||||
self.accountName = accountName
|
||||
self.oauthToken = oauthToken
|
||||
|
|
|
@ -54,10 +54,18 @@ public struct AppAccountsSelectorView: View {
|
|||
|
||||
@ViewBuilder
|
||||
private var labelView: some View {
|
||||
if let avatar = currentAccount.account?.avatar {
|
||||
AvatarView(url: avatar, size: avatarSize)
|
||||
} else {
|
||||
EmptyView()
|
||||
Group {
|
||||
if let avatar = currentAccount.account?.avatar {
|
||||
AvatarView(url: avatar, size: avatarSize)
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}.overlay(alignment: .topTrailing) {
|
||||
if !currentAccount.followRequests.isEmpty {
|
||||
Circle()
|
||||
.fill(Color.red)
|
||||
.frame(width: 9, height: 9)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import SwiftUI
|
||||
import Models
|
||||
import DesignSystem
|
||||
import Network
|
||||
import Env
|
||||
import Models
|
||||
import Network
|
||||
import NukeUI
|
||||
import SwiftUI
|
||||
|
||||
public struct ConversationDetailView: View {
|
||||
private enum Constants {
|
||||
|
@ -72,7 +72,8 @@ public struct ConversationDetailView: View {
|
|||
.toolbar {
|
||||
ToolbarItem(placement: .principal) {
|
||||
if viewModel.conversation.accounts.count == 1,
|
||||
let account = viewModel.conversation.accounts.first {
|
||||
let account = viewModel.conversation.accounts.first
|
||||
{
|
||||
EmojiTextApp(.init(stringValue: account.safeDisplayName), emojis: account.emojis)
|
||||
.font(.scaledHeadline)
|
||||
} else {
|
||||
|
|
|
@ -27,15 +27,13 @@ class ConversationDetailViewModel: ObservableObject {
|
|||
isLoadingMessages = false
|
||||
messages.insert(contentsOf: context.ancestors, at: 0)
|
||||
messages.append(contentsOf: context.descendants)
|
||||
} catch {
|
||||
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
func postMessage() async {
|
||||
guard let client else { return }
|
||||
isSendingMessage = true
|
||||
var finalText = conversation.accounts.map{ "@\($0.acct)" }.joined(separator: " ")
|
||||
var finalText = conversation.accounts.map { "@\($0.acct)" }.joined(separator: " ")
|
||||
finalText += " "
|
||||
finalText += newMessageText
|
||||
let data = StatusData(status: finalText,
|
||||
|
@ -55,14 +53,17 @@ class ConversationDetailViewModel: ObservableObject {
|
|||
|
||||
func handleEvent(event: any StreamEvent) {
|
||||
if let event = event as? StreamEventStatusUpdate,
|
||||
let index = messages.firstIndex(where: { $0.id == event.status.id }) {
|
||||
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 }) {
|
||||
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
|
||||
event.conversation.id == conversation.id
|
||||
{
|
||||
conversation = event.conversation
|
||||
appendNewStatus(status: conversation.lastStatus)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import SwiftUI
|
||||
import Env
|
||||
import DesignSystem
|
||||
import Network
|
||||
import Env
|
||||
import Models
|
||||
import Network
|
||||
import NukeUI
|
||||
import SwiftUI
|
||||
|
||||
struct ConversationMessageView: View {
|
||||
@EnvironmentObject private var quickLook: QuickLook
|
||||
|
@ -104,7 +104,7 @@ struct ConversationMessageView: View {
|
|||
withAnimation {
|
||||
isLiked = status.favourited == true
|
||||
}
|
||||
} catch { }
|
||||
} catch {}
|
||||
}
|
||||
} label: {
|
||||
Label(isLiked ? "status.action.unfavorite" : "status.action.favorite",
|
||||
|
|
|
@ -8,9 +8,10 @@ public class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate {
|
|||
}
|
||||
|
||||
public func scene(_ scene: UIScene,
|
||||
willConnectTo session: UISceneSession,
|
||||
options connectionOptions: UIScene.ConnectionOptions) {
|
||||
willConnectTo _: UISceneSession,
|
||||
options _: UIScene.ConnectionOptions)
|
||||
{
|
||||
guard let windowScene = scene as? UIWindowScene else { return }
|
||||
self.window = windowScene.keyWindow
|
||||
window = windowScene.keyWindow
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,6 +34,6 @@ public struct EmojiTextApp: View {
|
|||
|
||||
private func isRTL() -> Bool {
|
||||
// Arabic, Hebrew, Persian, Urdu, Kurdish, Azeri, Dhivehi
|
||||
return ["ar", "he", "fa", "ur", "ku", "az", "dv"].contains(self.language)
|
||||
return ["ar", "he", "fa", "ur", "ku", "az", "dv"].contains(language)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
import Env
|
||||
import Models
|
||||
import SwiftUI
|
||||
|
||||
public struct FollowRequestButtons: View {
|
||||
@EnvironmentObject private var currentAccount: CurrentAccount
|
||||
|
||||
let account: Account
|
||||
let requestUpdated: (() -> Void)?
|
||||
|
||||
public init(account: Account, requestUpdated: (() -> Void)? = nil) {
|
||||
self.account = account
|
||||
self.requestUpdated = requestUpdated
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
HStack {
|
||||
Button {
|
||||
Task {
|
||||
await currentAccount.acceptFollowerRequest(id: account.id)
|
||||
requestUpdated?()
|
||||
}
|
||||
} label: {
|
||||
Text("account.follow-request.accept")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
Button {
|
||||
Task {
|
||||
await currentAccount.rejectFollowerRequest(id: account.id)
|
||||
requestUpdated?()
|
||||
}
|
||||
} label: {
|
||||
Text("account.follow-request.reject")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
.disabled(currentAccount.isUpdating)
|
||||
.padding(.top, 4)
|
||||
}
|
||||
}
|
|
@ -7,6 +7,8 @@ public class CurrentAccount: ObservableObject {
|
|||
@Published public private(set) var account: Account?
|
||||
@Published public private(set) var lists: [List] = []
|
||||
@Published public private(set) var tags: [Tag] = []
|
||||
@Published public private(set) var followRequests: [Account] = []
|
||||
@Published public private(set) var isUpdating: Bool = false
|
||||
|
||||
private var client: Client?
|
||||
|
||||
|
@ -28,6 +30,7 @@ public class CurrentAccount: ObservableObject {
|
|||
group.addTask { await self.fetchCurrentAccount() }
|
||||
group.addTask { await self.fetchLists() }
|
||||
group.addTask { await self.fetchFollowedTags() }
|
||||
group.addTask { await self.fetchFollowerRequests() }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -103,4 +106,37 @@ public class CurrentAccount: ObservableObject {
|
|||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func fetchFollowerRequests() async {
|
||||
guard let client else { return }
|
||||
do {
|
||||
followRequests = try await client.get(endpoint: FollowRequests.list)
|
||||
} catch {
|
||||
followRequests = []
|
||||
}
|
||||
}
|
||||
|
||||
public func acceptFollowerRequest(id: String) async {
|
||||
guard let client else { return }
|
||||
do {
|
||||
isUpdating = true
|
||||
defer {
|
||||
isUpdating = false
|
||||
}
|
||||
_ = try await client.post(endpoint: FollowRequests.accept(id: id))
|
||||
await fetchFollowerRequests()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
public func rejectFollowerRequest(id: String) async {
|
||||
guard let client else { return }
|
||||
do {
|
||||
isUpdating = true
|
||||
defer {
|
||||
isUpdating = false
|
||||
}
|
||||
_ = try await client.post(endpoint: FollowRequests.reject(id: id))
|
||||
await fetchFollowerRequests()
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -71,7 +71,6 @@ public class RouterPath: ObservableObject {
|
|||
// That is on the same host as the person that posted the tag,
|
||||
// i.e. not a link that matches the pattern but elsewhere on the internet
|
||||
// In those circumstances, hijack the link and goto the tags page instead
|
||||
|
||||
navigate(to: .hashTag(tag: tag, account: nil))
|
||||
return .handled
|
||||
} else if let mention = status.mentions.first(where: { $0.url == url }) {
|
||||
|
|
|
@ -146,7 +146,7 @@ public class Client: ObservableObject, Equatable {
|
|||
logResponseOnError(httpResponse: httpResponse, data: data)
|
||||
do {
|
||||
return try decoder.decode(Entity.self, from: data)
|
||||
} catch let error {
|
||||
} catch {
|
||||
if let serverError = try? decoder.decode(ServerError.self, from: data) {
|
||||
throw serverError
|
||||
}
|
||||
|
|
|
@ -109,7 +109,7 @@ public enum Accounts: Endpoint {
|
|||
case let .follow(_, notify, reblogs):
|
||||
return [
|
||||
.init(name: "notify", value: notify ? "true" : "false"),
|
||||
.init(name: "reblogs", value: reblogs ? "true" : "false")
|
||||
.init(name: "reblogs", value: reblogs ? "true" : "false"),
|
||||
]
|
||||
case let .familiarFollowers(withAccount):
|
||||
return [.init(name: "id[]", value: withAccount)]
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
import Foundation
|
||||
|
||||
public enum FollowRequests: Endpoint {
|
||||
case list
|
||||
case accept(id: String)
|
||||
case reject(id: String)
|
||||
|
||||
public func path() -> String {
|
||||
switch self {
|
||||
case .list:
|
||||
return "follow_requests"
|
||||
case let .accept(id):
|
||||
return "follow_requests/\(id)/authorize"
|
||||
case let .reject(id):
|
||||
return "follow_requests/\(id)/reject"
|
||||
}
|
||||
}
|
||||
|
||||
public func queryItems() -> [URLQueryItem]? {
|
||||
nil
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ import Status
|
|||
import SwiftUI
|
||||
|
||||
struct NotificationRowView: View {
|
||||
@EnvironmentObject private var currentAccount: CurrentAccount
|
||||
@EnvironmentObject private var theme: Theme
|
||||
@EnvironmentObject private var routerPath: RouterPath
|
||||
@Environment(\.redactionReasons) private var reasons
|
||||
|
@ -19,6 +20,11 @@ struct NotificationRowView: View {
|
|||
VStack(alignment: .leading, spacing: 2) {
|
||||
makeMainLabel(type: type)
|
||||
makeContent(type: type)
|
||||
if type == .follow_request,
|
||||
currentAccount.followRequests.map(\.id).contains(notification.account.id)
|
||||
{
|
||||
FollowRequestButtons(account: notification.account)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import Models
|
||||
import PhotosUI
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
struct StatusEditorMediaContainer: Identifiable {
|
||||
let id = UUID().uuidString
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import AVKit
|
||||
import DesignSystem
|
||||
import Env
|
||||
import Models
|
||||
import NukeUI
|
||||
import SwiftUI
|
||||
import AVKit
|
||||
|
||||
struct StatusEditorMediaView: View {
|
||||
@EnvironmentObject private var theme: Theme
|
||||
|
@ -108,7 +108,8 @@ struct StatusEditorMediaView: View {
|
|||
ProgressView()
|
||||
}
|
||||
if mediaAttachement.url != nil,
|
||||
mediaAttachement.supportedType == .video || mediaAttachement.supportedType == .gifv {
|
||||
mediaAttachement.supportedType == .video || mediaAttachement.supportedType == .gifv
|
||||
{
|
||||
Image(systemName: "play.fill")
|
||||
.font(.headline)
|
||||
.tint(.white)
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import Foundation
|
||||
import PhotosUI
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
import UniformTypeIdentifiers
|
||||
import SwiftUI
|
||||
import PhotosUI
|
||||
|
||||
@MainActor
|
||||
enum StatusEditorUTTypeSupported: String, CaseIterable {
|
||||
|
@ -58,7 +58,7 @@ enum StatusEditorUTTypeSupported: String, CaseIterable {
|
|||
return await withCheckedContinuation { continuation in
|
||||
_ = item.loadTransferable(type: MovieFileTranseferable.self) { result in
|
||||
switch result {
|
||||
case .success(let success):
|
||||
case let .success(success):
|
||||
continuation.resume(with: .success(success))
|
||||
case .failure:
|
||||
continuation.resume(with: .success(nil))
|
||||
|
@ -77,7 +77,7 @@ struct MovieFileTranseferable: Transferable {
|
|||
} importing: { received in
|
||||
let copy = URL.temporaryDirectory.appending(path: "\(UUID().uuidString).\(received.file.pathExtension)")
|
||||
try FileManager.default.copyItem(at: received.file, to: copy)
|
||||
return Self.init(url: copy)
|
||||
return Self(url: copy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -95,14 +95,14 @@ struct ImageFileTranseferable: Transferable {
|
|||
} importing: { received in
|
||||
let copy = URL.temporaryDirectory.appending(path: "\(UUID().uuidString).\(received.file.pathExtension)")
|
||||
try FileManager.default.copyItem(at: received.file, to: copy)
|
||||
return Self.init(url: copy)
|
||||
return Self(url: copy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension URL {
|
||||
public func mimeType() -> String {
|
||||
if let mimeType = UTType(filenameExtension: self.pathExtension)?.preferredMIMEType {
|
||||
public extension URL {
|
||||
func mimeType() -> String {
|
||||
if let mimeType = UTType(filenameExtension: pathExtension)?.preferredMIMEType {
|
||||
return mimeType
|
||||
} else {
|
||||
return "application/octet-stream"
|
||||
|
|
|
@ -96,10 +96,10 @@ public struct StatusEditorView: View {
|
|||
.alert("Error while posting",
|
||||
isPresented: $viewModel.showPostingErrorAlert,
|
||||
actions: {
|
||||
Button("Ok") { }
|
||||
}, message: {
|
||||
Text(viewModel.postingError ?? "")
|
||||
})
|
||||
Button("Ok") {}
|
||||
}, message: {
|
||||
Text(viewModel.postingError ?? "")
|
||||
})
|
||||
.toolbar {
|
||||
if preferences.isOpenAIEnabled {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
|
|
|
@ -143,7 +143,7 @@ public class StatusEditorViewModel: ObservableObject {
|
|||
}
|
||||
isPosting = false
|
||||
return postStatus
|
||||
} catch let error {
|
||||
} catch {
|
||||
if let error = error as? Models.ServerError {
|
||||
postingError = error.error
|
||||
showPostingErrorAlert = true
|
||||
|
@ -468,7 +468,8 @@ public class StatusEditorViewModel: ObservableObject {
|
|||
}
|
||||
|
||||
if var imageFile = file as? ImageFileTranseferable,
|
||||
let image = imageFile.image {
|
||||
let image = imageFile.image
|
||||
{
|
||||
medias.append(.init(image: image,
|
||||
movieTransferable: nil,
|
||||
mediaAttachment: nil,
|
||||
|
@ -511,22 +512,24 @@ public class StatusEditorViewModel: ObservableObject {
|
|||
do {
|
||||
if let index = indexOf(container: newContainer) {
|
||||
if let image = originalContainer.image,
|
||||
let data = image.jpegData(compressionQuality: 0.90) {
|
||||
let data = image.jpegData(compressionQuality: 0.90)
|
||||
{
|
||||
let uploadedMedia = try await uploadMedia(data: data, mimeType: "image/jpeg")
|
||||
mediasImages[index] = .init(image: mode.isInShareExtension ? originalContainer.image : nil,
|
||||
movieTransferable: nil,
|
||||
mediaAttachment: uploadedMedia,
|
||||
error: nil)
|
||||
mediasImages[index] = .init(image: mode.isInShareExtension ? originalContainer.image : nil,
|
||||
movieTransferable: nil,
|
||||
mediaAttachment: uploadedMedia,
|
||||
error: nil)
|
||||
if let uploadedMedia, uploadedMedia.url == nil {
|
||||
scheduleAsyncMediaRefresh(mediaAttachement: uploadedMedia)
|
||||
}
|
||||
} else if let videoURL = originalContainer.movieTransferable?.url,
|
||||
let data = try? Data(contentsOf: videoURL) {
|
||||
let data = try? Data(contentsOf: videoURL)
|
||||
{
|
||||
let uploadedMedia = try await uploadMedia(data: data, mimeType: videoURL.mimeType())
|
||||
mediasImages[index] = .init(image: mode.isInShareExtension ? originalContainer.image : nil,
|
||||
movieTransferable: originalContainer.movieTransferable,
|
||||
mediaAttachment: uploadedMedia,
|
||||
error: nil)
|
||||
movieTransferable: originalContainer.movieTransferable,
|
||||
mediaAttachment: uploadedMedia,
|
||||
error: nil)
|
||||
if let uploadedMedia, uploadedMedia.url == nil {
|
||||
scheduleAsyncMediaRefresh(mediaAttachement: uploadedMedia)
|
||||
}
|
||||
|
@ -547,13 +550,14 @@ public class StatusEditorViewModel: ObservableObject {
|
|||
Task {
|
||||
repeat {
|
||||
if let client,
|
||||
let index = mediasImages.firstIndex(where: { $0.mediaAttachment?.id == mediaAttachement.id }) {
|
||||
let index = mediasImages.firstIndex(where: { $0.mediaAttachment?.id == mediaAttachement.id })
|
||||
{
|
||||
guard mediasImages[index].mediaAttachment?.url == nil else {
|
||||
return
|
||||
}
|
||||
do {
|
||||
let newAttachement: MediaAttachment = try await client.get(endpoint: Media.media(id: mediaAttachement.id,
|
||||
description: nil))
|
||||
description: nil))
|
||||
if newAttachement.url != nil {
|
||||
let oldContainer = mediasImages[index]
|
||||
mediasImages[index] = .init(image: oldContainer.image,
|
||||
|
@ -561,10 +565,10 @@ public class StatusEditorViewModel: ObservableObject {
|
|||
mediaAttachment: newAttachement,
|
||||
error: nil)
|
||||
}
|
||||
} catch { }
|
||||
} catch {}
|
||||
}
|
||||
try? await Task.sleep(for: .seconds(5))
|
||||
} while (!Task.isCancelled)
|
||||
} while !Task.isCancelled
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -77,7 +77,8 @@ struct StatusRowContextMenu: View {
|
|||
}
|
||||
|
||||
if let lang = preferences.serverPreferences?.postLanguage ?? Locale.current.language.languageCode?.identifier,
|
||||
viewModel.status.language != lang {
|
||||
viewModel.status.language != lang
|
||||
{
|
||||
Button {
|
||||
Task {
|
||||
await viewModel.translate(userLang: lang)
|
||||
|
|
|
@ -365,7 +365,8 @@ public struct StatusRowView: View {
|
|||
private var embedStatusView: some View {
|
||||
if !reasons.contains(.placeholder) {
|
||||
if !viewModel.isCompact, !viewModel.isEmbedLoading,
|
||||
let embed = viewModel.embeddedStatus {
|
||||
let embed = viewModel.embeddedStatus
|
||||
{
|
||||
StatusEmbeddedView(status: embed)
|
||||
} else if viewModel.isEmbedLoading, !viewModel.isCompact {
|
||||
StatusEmbeddedView(status: .placeholder())
|
||||
|
|
Loading…
Reference in a new issue