Add hashtags lists.
This commit is contained in:
parent
9dcbd5336b
commit
5736734695
|
@ -6,16 +6,16 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
/// Search results.
|
||||
public struct Result: Codable {
|
||||
/// Represents the results of a search.
|
||||
public struct SearchResults: Codable {
|
||||
|
||||
/// List of accoutns.
|
||||
/// Accounts which match the given query.
|
||||
public let accounts: [Account]
|
||||
|
||||
/// List od statuses.
|
||||
/// Statuses which match the given query.
|
||||
public let statuses: [Status]
|
||||
|
||||
|
||||
/// Hashtags which match the given query
|
||||
public let hashtags: [Tag]
|
||||
|
||||
public enum CodingKeys: CodingKey {
|
|
@ -0,0 +1,20 @@
|
|||
//
|
||||
// https://mczachurski.dev
|
||||
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension MastodonClientAuthenticated {
|
||||
|
||||
func search(query: String, type: Mastodon.Search.ResultsType) async throws -> SearchResults {
|
||||
let request = try Self.request(
|
||||
for: baseURL,
|
||||
target: Mastodon.Search.search(query, type, false),
|
||||
withBearerToken: token
|
||||
)
|
||||
|
||||
return try await downloadJson(SearchResults.self, request: request)
|
||||
}
|
||||
}
|
|
@ -88,9 +88,11 @@ public class MastodonClientAuthenticated: MastodonClientProtocol {
|
|||
|
||||
public func downloadJson<T>(_ type: T.Type, request: URLRequest) async throws -> T where T: Decodable {
|
||||
let (data, response) = try await urlSession.data(for: request)
|
||||
|
||||
guard (response as? HTTPURLResponse)?.status?.responseType == .success else {
|
||||
throw NetworkError.notSuccessResponse(response)
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
do {
|
||||
return try JSONDecoder().decode(type, from: data)
|
||||
|
|
|
@ -8,13 +8,19 @@ import Foundation
|
|||
|
||||
extension Mastodon {
|
||||
public enum Search {
|
||||
case search(SearchQuery, Bool)
|
||||
case search(SearchQuery, ResultsType, Bool)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
extension Mastodon.Search: TargetType {
|
||||
fileprivate var apiPath: String { return "/api/v1/search" }
|
||||
public enum ResultsType: String {
|
||||
case accounts = "accounts"
|
||||
case hashtags = "hashtags"
|
||||
case statuses = "statuses"
|
||||
}
|
||||
|
||||
fileprivate var apiPath: String { return "/api/v2/search" }
|
||||
|
||||
/// The path to be appended to `baseURL` to form the full `URL`.
|
||||
public var path: String {
|
||||
|
@ -35,9 +41,10 @@ extension Mastodon.Search: TargetType {
|
|||
/// The parameters to be incoded in the request.
|
||||
public var queryItems: [(String, String)]? {
|
||||
switch self {
|
||||
case .search(let query, let resolveNonLocal):
|
||||
case .search(let query, let resultsType, let resolveNonLocal):
|
||||
return [
|
||||
("q", query),
|
||||
("type", resultsType.rawValue),
|
||||
("resolve", resolveNonLocal.asString)
|
||||
]
|
||||
}
|
||||
|
|
|
@ -68,11 +68,17 @@
|
|||
F88C2475295C37BB0006098B /* CoreDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C2474295C37BB0006098B /* CoreDataHandler.swift */; };
|
||||
F88C2478295C37BB0006098B /* Vernissage.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = F88C2476295C37BB0006098B /* Vernissage.xcdatamodeld */; };
|
||||
F88C2482295C3A4F0006098B /* StatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C2481295C3A4F0006098B /* StatusView.swift */; };
|
||||
F88C2486295C48030006098B /* HTMLFotmattedText.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C2485295C48030006098B /* HTMLFotmattedText.swift */; };
|
||||
F88E4D42297E69FD0057491A /* StatusesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88E4D41297E69FD0057491A /* StatusesView.swift */; };
|
||||
F88E4D44297E82EB0057491A /* Status+MediaAttachmentType.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88E4D43297E82EB0057491A /* Status+MediaAttachmentType.swift */; };
|
||||
F88E4D46297E89DF0057491A /* TrendsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88E4D45297E89DF0057491A /* TrendsService.swift */; };
|
||||
F88E4D48297E90CD0057491A /* TrendStatusesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88E4D47297E90CD0057491A /* TrendStatusesView.swift */; };
|
||||
F88E4D4A297EA0490057491A /* RouterPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88E4D49297EA0490057491A /* RouterPath.swift */; };
|
||||
F88E4D4D297EA4290057491A /* EmojiText in Frameworks */ = {isa = PBXBuildFile; productRef = F88E4D4C297EA4290057491A /* EmojiText */; };
|
||||
F88E4D50297EA5230057491A /* HTML2Markdown in Frameworks */ = {isa = PBXBuildFile; productRef = F88E4D4F297EA5230057491A /* HTML2Markdown */; };
|
||||
F88E4D52297EA6DA0057491A /* String+Markdown.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88E4D51297EA6DA0057491A /* String+Markdown.swift */; };
|
||||
F88E4D54297EA7EE0057491A /* MarkdownFormattedText.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88E4D53297EA7EE0057491A /* MarkdownFormattedText.swift */; };
|
||||
F88E4D56297EAD6E0057491A /* View+Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88E4D55297EAD6E0057491A /* View+Router.swift */; };
|
||||
F88E4D5A297ECEE60057491A /* SearchService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88E4D59297ECEE60057491A /* SearchService.swift */; };
|
||||
F88FAD21295F3944009B20C9 /* HomeFeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD20295F3944009B20C9 /* HomeFeedView.swift */; };
|
||||
F88FAD27295F400E009B20C9 /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD26295F400E009B20C9 /* NotificationsView.swift */; };
|
||||
F88FAD2A295F43B8009B20C9 /* AccountData+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD28295F43B8009B20C9 /* AccountData+CoreDataClass.swift */; };
|
||||
|
@ -169,11 +175,15 @@
|
|||
F88C2474295C37BB0006098B /* CoreDataHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataHandler.swift; sourceTree = "<group>"; };
|
||||
F88C2477295C37BB0006098B /* Vernissage.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Vernissage.xcdatamodel; sourceTree = "<group>"; };
|
||||
F88C2481295C3A4F0006098B /* StatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusView.swift; sourceTree = "<group>"; };
|
||||
F88C2485295C48030006098B /* HTMLFotmattedText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTMLFotmattedText.swift; sourceTree = "<group>"; };
|
||||
F88E4D41297E69FD0057491A /* StatusesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusesView.swift; sourceTree = "<group>"; };
|
||||
F88E4D43297E82EB0057491A /* Status+MediaAttachmentType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Status+MediaAttachmentType.swift"; sourceTree = "<group>"; };
|
||||
F88E4D45297E89DF0057491A /* TrendsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendsService.swift; sourceTree = "<group>"; };
|
||||
F88E4D47297E90CD0057491A /* TrendStatusesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendStatusesView.swift; sourceTree = "<group>"; };
|
||||
F88E4D49297EA0490057491A /* RouterPath.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouterPath.swift; sourceTree = "<group>"; };
|
||||
F88E4D51297EA6DA0057491A /* String+Markdown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Markdown.swift"; sourceTree = "<group>"; };
|
||||
F88E4D53297EA7EE0057491A /* MarkdownFormattedText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkdownFormattedText.swift; sourceTree = "<group>"; };
|
||||
F88E4D55297EAD6E0057491A /* View+Router.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Router.swift"; sourceTree = "<group>"; };
|
||||
F88E4D59297ECEE60057491A /* SearchService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchService.swift; sourceTree = "<group>"; };
|
||||
F88FAD20295F3944009B20C9 /* HomeFeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeFeedView.swift; sourceTree = "<group>"; };
|
||||
F88FAD26295F400E009B20C9 /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = "<group>"; };
|
||||
F88FAD28295F43B8009B20C9 /* AccountData+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountData+CoreDataClass.swift"; sourceTree = "<group>"; };
|
||||
|
@ -215,10 +225,12 @@
|
|||
files = (
|
||||
F89992C7296D3DF8005994BF /* MastodonKit in Frameworks */,
|
||||
F8210DD52966BB7E001D9973 /* Nuke in Frameworks */,
|
||||
F88E4D4D297EA4290057491A /* EmojiText in Frameworks */,
|
||||
F8210DD72966BB7E001D9973 /* NukeExtensions in Frameworks */,
|
||||
F8210DD92966BB7E001D9973 /* NukeUI in Frameworks */,
|
||||
F85E132529741F05006A051D /* ActivityIndicatorView in Frameworks */,
|
||||
F8B1E64F2973F61400EE0D10 /* Drops in Frameworks */,
|
||||
F88E4D50297EA5230057491A /* HTML2Markdown in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -274,6 +286,7 @@
|
|||
F8341F8F295C636C009C8EE6 /* Data+Exif.swift */,
|
||||
F85D49862964334100751DF7 /* String+Date.swift */,
|
||||
F8C14391296AF0B3001FE31D /* String+Exif.swift */,
|
||||
F88E4D51297EA6DA0057491A /* String+Markdown.swift */,
|
||||
F898DE6F2972868A004B4A6A /* String+Empty.swift */,
|
||||
F8210DE42966E160001D9973 /* Color+SystemColors.swift */,
|
||||
F8210DE62966E1D1001D9973 /* Color+Assets.swift */,
|
||||
|
@ -281,6 +294,7 @@
|
|||
F8984E4C296B648000A2610F /* UIImage+Blurhash.swift */,
|
||||
F8996DEA2971D29D0043EEC6 /* View+Transition.swift */,
|
||||
F88E4D43297E82EB0057491A /* Status+MediaAttachmentType.swift */,
|
||||
F88E4D55297EAD6E0057491A /* View+Router.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
|
@ -320,14 +334,6 @@
|
|||
path = CoreData;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F8341F97295C6434009C8EE6 /* Formatters */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F88C2485295C48030006098B /* HTMLFotmattedText.swift */,
|
||||
);
|
||||
path = Formatters;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
F83901A2295D863B00456AE2 /* Widgets */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -349,6 +355,7 @@
|
|||
F86B7222296C4BF500EE59EC /* ContentWarning.swift */,
|
||||
F89D6C49297196FF001DA3D4 /* ImagesViewer.swift */,
|
||||
F857F9FC297D8ED3002C109C /* ActionMenu.swift */,
|
||||
F88E4D53297EA7EE0057491A /* MarkdownFormattedText.swift */,
|
||||
);
|
||||
path = Widgets;
|
||||
sourceTree = "<group>";
|
||||
|
@ -400,7 +407,6 @@
|
|||
F8210DE82966E4D8001D9973 /* Modifiers */,
|
||||
F88FAD30295F5010009B20C9 /* Services */,
|
||||
F83901A2295D863B00456AE2 /* Widgets */,
|
||||
F8341F97295C6434009C8EE6 /* Formatters */,
|
||||
F8341F96295C6427009C8EE6 /* CoreData */,
|
||||
F8341F95295C640C009C8EE6 /* Models */,
|
||||
F8341F94295C63FE009C8EE6 /* Extensions */,
|
||||
|
@ -437,6 +443,8 @@
|
|||
F886F256297859E300879356 /* CacheImageService.swift */,
|
||||
F8163775297C3E3D00E6E04A /* PublicTimelineService.swift */,
|
||||
F88E4D45297E89DF0057491A /* TrendsService.swift */,
|
||||
F88E4D49297EA0490057491A /* RouterPath.swift */,
|
||||
F88E4D59297ECEE60057491A /* SearchService.swift */,
|
||||
);
|
||||
path = Services;
|
||||
sourceTree = "<group>";
|
||||
|
@ -518,6 +526,8 @@
|
|||
F89992C6296D3DF8005994BF /* MastodonKit */,
|
||||
F8B1E64E2973F61400EE0D10 /* Drops */,
|
||||
F85E132429741F05006A051D /* ActivityIndicatorView */,
|
||||
F88E4D4C297EA4290057491A /* EmojiText */,
|
||||
F88E4D4F297EA5230057491A /* HTML2Markdown */,
|
||||
);
|
||||
productName = Vernissage;
|
||||
productReference = F88C2468295C37B80006098B /* Vernissage.app */;
|
||||
|
@ -551,6 +561,8 @@
|
|||
F8210DD32966BB7E001D9973 /* XCRemoteSwiftPackageReference "Nuke" */,
|
||||
F8B1E64D2973F61400EE0D10 /* XCRemoteSwiftPackageReference "Drops" */,
|
||||
F85E132329741F05006A051D /* XCRemoteSwiftPackageReference "ActivityIndicatorView" */,
|
||||
F88E4D4B297EA4290057491A /* XCRemoteSwiftPackageReference "EmojiText" */,
|
||||
F88E4D4E297EA5230057491A /* XCRemoteSwiftPackageReference "HTML2Markdown" */,
|
||||
);
|
||||
productRefGroup = F88C2469295C37B80006098B /* Products */;
|
||||
projectDirPath = "";
|
||||
|
@ -610,12 +622,14 @@
|
|||
F88E4D42297E69FD0057491A /* StatusesView.swift in Sources */,
|
||||
F85D497929640B9D00751DF7 /* ImagesCarousel.swift in Sources */,
|
||||
F89D6C3F29716E41001DA3D4 /* Theme.swift in Sources */,
|
||||
F88E4D5A297ECEE60057491A /* SearchService.swift in Sources */,
|
||||
F8CC95CE2970761D00C9C2AC /* TintColor.swift in Sources */,
|
||||
F89992CC296D9231005994BF /* StatusViewModel.swift in Sources */,
|
||||
F80048052961850500E6868A /* StatusData+CoreDataClass.swift in Sources */,
|
||||
F86B7221296C49A300EE59EC /* EmptyButtonStyle.swift in Sources */,
|
||||
F80048042961850500E6868A /* AttachmentData+CoreDataProperties.swift in Sources */,
|
||||
F86B7223296C4BF500EE59EC /* ContentWarning.swift in Sources */,
|
||||
F88E4D4A297EA0490057491A /* RouterPath.swift in Sources */,
|
||||
F83901A6295D8EC000456AE2 /* LabelIcon.swift in Sources */,
|
||||
F8B1E6512973FB7E00EE0D10 /* ToastrService.swift in Sources */,
|
||||
F88E4D48297E90CD0057491A /* TrendStatusesView.swift in Sources */,
|
||||
|
@ -630,6 +644,7 @@
|
|||
F88C246E295C37B80006098B /* MainView.swift in Sources */,
|
||||
F86B721E296C458700EE59EC /* BlurredImage.swift in Sources */,
|
||||
F88C2478295C37BB0006098B /* Vernissage.xcdatamodeld in Sources */,
|
||||
F88E4D52297EA6DA0057491A /* String+Markdown.swift in Sources */,
|
||||
F898DE7229728CB2004B4A6A /* CommentViewModel.swift in Sources */,
|
||||
F89A46DE296EABA20062125F /* StatusPlaceholder.swift in Sources */,
|
||||
F88C2482295C3A4F0006098B /* StatusView.swift in Sources */,
|
||||
|
@ -637,7 +652,6 @@
|
|||
F8996DEB2971D29D0043EEC6 /* View+Transition.swift in Sources */,
|
||||
F89D6C4629718193001DA3D4 /* ThemeSection.swift in Sources */,
|
||||
F85D497F296416C800751DF7 /* CommentsSection.swift in Sources */,
|
||||
F88C2486295C48030006098B /* HTMLFotmattedText.swift in Sources */,
|
||||
F866F6A529604194002E8F88 /* ApplicationSettingsHandler.swift in Sources */,
|
||||
F88ABD9229686F1C004EF61E /* MemoryCache.swift in Sources */,
|
||||
F857F9FD297D8ED3002C109C /* ActionMenu.swift in Sources */,
|
||||
|
@ -653,12 +667,14 @@
|
|||
F88C246C295C37B80006098B /* VernissageApp.swift in Sources */,
|
||||
F802884F297AEED5000BDD51 /* DatabaseError.swift in Sources */,
|
||||
F85D4971296402DC00751DF7 /* AuthorizationService.swift in Sources */,
|
||||
F88E4D56297EAD6E0057491A /* View+Router.swift in Sources */,
|
||||
F88FAD32295F5029009B20C9 /* RemoteFileService.swift in Sources */,
|
||||
F88FAD27295F400E009B20C9 /* NotificationsView.swift in Sources */,
|
||||
F86B7216296BFFDA00EE59EC /* UserProfileStatuses.swift in Sources */,
|
||||
F897978F29684BCB00B22335 /* LoadingView.swift in Sources */,
|
||||
F89992C9296D6DC7005994BF /* CommentBody.swift in Sources */,
|
||||
F88FAD2D295F4AD7009B20C9 /* ApplicationState.swift in Sources */,
|
||||
F88E4D54297EA7EE0057491A /* MarkdownFormattedText.swift in Sources */,
|
||||
F866F6A1296040A8002E8F88 /* ApplicationSettings+CoreDataProperties.swift in Sources */,
|
||||
F8C14394296AF21B001FE31D /* Double+Round.swift in Sources */,
|
||||
F89A46DC296EAACE0062125F /* SettingsView.swift in Sources */,
|
||||
|
@ -896,6 +912,22 @@
|
|||
minimumVersion = 1.0.0;
|
||||
};
|
||||
};
|
||||
F88E4D4B297EA4290057491A /* XCRemoteSwiftPackageReference "EmojiText" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/divadretlaw/EmojiText";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 1.3.0;
|
||||
};
|
||||
};
|
||||
F88E4D4E297EA5230057491A /* XCRemoteSwiftPackageReference "HTML2Markdown" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://gitlab.com/mflint/HTML2Markdown";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 1.0.0;
|
||||
};
|
||||
};
|
||||
F8B1E64D2973F61400EE0D10 /* XCRemoteSwiftPackageReference "Drops" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/omaralbeik/Drops";
|
||||
|
@ -927,6 +959,16 @@
|
|||
package = F85E132329741F05006A051D /* XCRemoteSwiftPackageReference "ActivityIndicatorView" */;
|
||||
productName = ActivityIndicatorView;
|
||||
};
|
||||
F88E4D4C297EA4290057491A /* EmojiText */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = F88E4D4B297EA4290057491A /* XCRemoteSwiftPackageReference "EmojiText" */;
|
||||
productName = EmojiText;
|
||||
};
|
||||
F88E4D4F297EA5230057491A /* HTML2Markdown */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = F88E4D4E297EA5230057491A /* XCRemoteSwiftPackageReference "HTML2Markdown" */;
|
||||
productName = HTML2Markdown;
|
||||
};
|
||||
F89992C6296D3DF8005994BF /* MastodonKit */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = MastodonKit;
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
//
|
||||
// https://mczachurski.dev
|
||||
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import HTML2Markdown
|
||||
|
||||
extension String {
|
||||
public var asMarkdown: String {
|
||||
do {
|
||||
let dom = try HTMLParser().parse(html: self)
|
||||
return dom.toMarkdown()
|
||||
// Add space between hashtags and mentions that follow each other
|
||||
.replacingOccurrences(of: ")[", with: ") [")
|
||||
} catch {
|
||||
return self
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
//
|
||||
// https://mczachurski.dev
|
||||
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
extension View {
|
||||
func withAppRouteur() -> some View {
|
||||
self.navigationDestination(for: RouteurDestinations.self) { destination in
|
||||
switch destination {
|
||||
case .tag(let hashTag):
|
||||
StatusesView(listType: .hashtag(tag: hashTag))
|
||||
case .status(let id, let blurhash, let metaImageWidth, let metaImageHeight):
|
||||
StatusView(statusId: id,
|
||||
imageBlurhash: blurhash,
|
||||
imageWidth: metaImageWidth,
|
||||
imageHeight: metaImageHeight)
|
||||
case .statuses(let listType):
|
||||
StatusesView(listType: listType)
|
||||
case .userProfile(let accountId, let accountDisplayName, let accountUserName):
|
||||
UserProfileView(
|
||||
accountId: accountId,
|
||||
accountDisplayName: accountDisplayName,
|
||||
accountUserName: accountUserName)
|
||||
case .accounts(let entityId, let listType):
|
||||
AccountsView(entityId: entityId, listType: listType)
|
||||
case .signIn:
|
||||
SignInView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func withSheetDestinations(sheetDestinations: Binding<SheetDestinations?>) -> some View {
|
||||
self.sheet(item: sheetDestinations) { destination in
|
||||
switch destination {
|
||||
case .replyToStatusEditor(let status):
|
||||
ComposeView(statusViewModel: status)
|
||||
case .newStatusEditor:
|
||||
ComposeView()
|
||||
case .settings:
|
||||
SettingsView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,82 +0,0 @@
|
|||
//
|
||||
// https://mczachurski.dev
|
||||
// Copyright © 2022 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
struct HTMLFormattedText: UIViewRepresentable {
|
||||
@EnvironmentObject var applicationState: ApplicationState
|
||||
|
||||
private let text: String
|
||||
private let textView = UITextView()
|
||||
private let fontSize: Int
|
||||
private let width: Int
|
||||
|
||||
init(_ content: String, withFontSize fontSize: Int = 16, andWidth width: Int? = nil) {
|
||||
self.text = content
|
||||
self.fontSize = fontSize
|
||||
self.width = width ?? Int(UIScreen.main.bounds.width) - 16
|
||||
}
|
||||
|
||||
func makeUIView(context: UIViewRepresentableContext<Self>) -> UITextView {
|
||||
textView.widthAnchor.constraint(equalToConstant: CGFloat(self.width)).isActive = true
|
||||
textView.isSelectable = false
|
||||
textView.isUserInteractionEnabled = false
|
||||
textView.translatesAutoresizingMaskIntoConstraints = false
|
||||
textView.isScrollEnabled = false
|
||||
textView.backgroundColor = UIColor(.clear)
|
||||
|
||||
return textView
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext<Self>) {
|
||||
Task { @MainActor in
|
||||
if let attributeText = self.converHTML(text: text) {
|
||||
textView.attributedText = attributeText
|
||||
} else {
|
||||
textView.text = String.empty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func converHTML(text: String) -> NSAttributedString?{
|
||||
guard let data = text.data(using: .utf16) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let largeAttributes = [
|
||||
NSAttributedString.Key.font: UIFont.systemFont(ofSize: CGFloat(self.fontSize)),
|
||||
NSAttributedString.Key.foregroundColor: UIColor(.mainTextColor)
|
||||
]
|
||||
|
||||
let linkAttributes = [
|
||||
NSAttributedString.Key.font: UIFont.systemFont(ofSize: CGFloat(self.fontSize)),
|
||||
NSAttributedString.Key.foregroundColor: UIColor(applicationState.tintColor.color())
|
||||
]
|
||||
|
||||
if let attributedString = try? NSMutableAttributedString(data: data,
|
||||
options: [.documentType: NSAttributedString.DocumentType.html],
|
||||
documentAttributes: nil) {
|
||||
attributedString.enumerateAttributes(in: NSRange(0..<attributedString.length)) { value, range, stop in
|
||||
attributedString.setAttributes(largeAttributes, range: range)
|
||||
|
||||
if value.keys.contains(NSAttributedString.Key.link) {
|
||||
attributedString.setAttributes(linkAttributes, range: range)
|
||||
}
|
||||
}
|
||||
|
||||
return attributedString
|
||||
} else{
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct HTMLFotmattedText_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
HTMLFormattedText("<p>Danish-made 1st class kebab</p><p>Say yes thanks to 2kg. delicious kebab, which is confused and cooked.</p><p>Yes thanks for 149.95</p><p>Now you can make the most delicious sandwiches, kebab mix and much more at home</p>")
|
||||
}
|
||||
}
|
|
@ -25,4 +25,20 @@ public class PublicTimelineService {
|
|||
let client = MastodonClient(baseURL: serverUrl).getAuthenticated(token: accessToken)
|
||||
return try await client.getPublicTimeline(local: local, remote: remote, onlyMedia: true, maxId: maxId, sinceId: sinceId, minId: minId, limit: limit)
|
||||
}
|
||||
|
||||
public func getTagStatuses(accountData: AccountData?,
|
||||
tag: String,
|
||||
local: Bool,
|
||||
remote: Bool,
|
||||
maxId: String? = nil,
|
||||
sinceId: String? = nil,
|
||||
minId: String? = nil,
|
||||
limit: Int = 40) async throws -> [Status] {
|
||||
guard let accessToken = accountData?.accessToken, let serverUrl = accountData?.serverUrl else {
|
||||
return []
|
||||
}
|
||||
|
||||
let client = MastodonClient(baseURL: serverUrl).getAuthenticated(token: accessToken)
|
||||
return try await client.getTagTimeline(tag: tag, local: local, remote: remote, onlyMedia: true, maxId: maxId, sinceId: sinceId, minId: minId, limit: limit)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
//
|
||||
// https://mczachurski.dev
|
||||
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Foundation
|
||||
import MastodonKit
|
||||
|
||||
enum RouteurDestinations: Hashable {
|
||||
case tag(hashTag: String)
|
||||
case status(id: String, blurhash: String? = nil, metaImageWidth: Int32? = nil, metaImageHeight: Int32? = nil)
|
||||
case statuses(listType: StatusesView.ListType)
|
||||
case userProfile(accountId: String, accountDisplayName: String?, accountUserName: String)
|
||||
case accounts(entityId: String, listType: AccountsView.ListType)
|
||||
case signIn
|
||||
}
|
||||
|
||||
enum SheetDestinations: Identifiable {
|
||||
case newStatusEditor
|
||||
case replyToStatusEditor(status: StatusViewModel)
|
||||
case settings
|
||||
|
||||
public var id: String {
|
||||
switch self {
|
||||
case .replyToStatusEditor, .newStatusEditor:
|
||||
return "statusEditor"
|
||||
case .settings:
|
||||
return "settings"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
class RouterPath: ObservableObject {
|
||||
public var urlHandler: ((URL) -> OpenURLAction.Result)?
|
||||
|
||||
@Published public var path: [RouteurDestinations] = []
|
||||
@Published public var presentedSheet: SheetDestinations?
|
||||
|
||||
public init() {}
|
||||
|
||||
public func navigate(to: RouteurDestinations) {
|
||||
path.append(to)
|
||||
}
|
||||
|
||||
public func handle(url: URL, accountData: AccountData? = nil) -> OpenURLAction.Result {
|
||||
if url.pathComponents.contains(where: { $0 == "tags" }), let tag = url.pathComponents.last {
|
||||
navigate(to: .tag(hashTag: tag))
|
||||
return .handled
|
||||
} else if url.lastPathComponent.first == "@", let host = url.host {
|
||||
let acct = "\(url.lastPathComponent)@\(host)"
|
||||
Task {
|
||||
await navigateToAccountFrom(acct: acct, url: url, accountData: accountData)
|
||||
}
|
||||
|
||||
return .handled
|
||||
}
|
||||
|
||||
return urlHandler?(url) ?? .systemAction
|
||||
}
|
||||
|
||||
public func navigateToAccountFrom(acct: String, url: URL, accountData: AccountData? = nil) async {
|
||||
guard let accountData else { return }
|
||||
|
||||
Task {
|
||||
let results = try? await SearchService.shared.search(accountData: accountData,
|
||||
query: acct,
|
||||
resultsType: Mastodon.Search.ResultsType.accounts)
|
||||
|
||||
if let account = results?.accounts.first {
|
||||
navigate(to: .userProfile(accountId: account.id, accountDisplayName: account.displayNameWithoutEmojis, accountUserName: account.acct))
|
||||
} else {
|
||||
await UIApplication.shared.open(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
//
|
||||
// https://mczachurski.dev
|
||||
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MastodonKit
|
||||
|
||||
public class SearchService {
|
||||
public static let shared = SearchService()
|
||||
private init() { }
|
||||
|
||||
public func search(accountData: AccountData?,
|
||||
query: String,
|
||||
resultsType: Mastodon.Search.ResultsType) async throws -> SearchResults? {
|
||||
guard let accessToken = accountData?.accessToken, let serverUrl = accountData?.serverUrl else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let client = MastodonClient(baseURL: serverUrl).getAuthenticated(token: accessToken)
|
||||
return try await client.search(query: query, type: resultsType)
|
||||
}
|
||||
}
|
|
@ -11,34 +11,37 @@ struct VernissageApp: App {
|
|||
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
||||
|
||||
let coreDataHandler = CoreDataHandler.shared
|
||||
let applicationState = ApplicationState.shared
|
||||
@StateObject var applicationState = ApplicationState.shared
|
||||
|
||||
@State var applicationViewMode: ApplicationViewMode = .loading
|
||||
@State var tintColor = ApplicationState.shared.tintColor.color()
|
||||
@State var theme = ApplicationState.shared.theme.colorScheme()
|
||||
|
||||
@StateObject private var routerPath = RouterPath()
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
NavigationStack {
|
||||
NavigationStack(path: $routerPath.path) {
|
||||
switch applicationViewMode {
|
||||
case .loading:
|
||||
LoadingView()
|
||||
.withAppRouteur()
|
||||
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
|
||||
case .signIn:
|
||||
SignInView { viewMode in
|
||||
applicationViewMode = viewMode
|
||||
}
|
||||
.environment(\.managedObjectContext, coreDataHandler.container.viewContext)
|
||||
.environmentObject(applicationState)
|
||||
.withAppRouteur()
|
||||
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
|
||||
case .mainView:
|
||||
MainView { color in
|
||||
self.tintColor = color.color()
|
||||
} onThemeChange: { theme in
|
||||
self.theme = theme.colorScheme()
|
||||
}
|
||||
.environment(\.managedObjectContext, coreDataHandler.container.viewContext)
|
||||
.environmentObject(applicationState)
|
||||
MainView()
|
||||
.withAppRouteur()
|
||||
.withSheetDestinations(sheetDestinations: $routerPath.presentedSheet)
|
||||
}
|
||||
}
|
||||
.environment(\.managedObjectContext, coreDataHandler.container.viewContext)
|
||||
.environmentObject(applicationState)
|
||||
.environmentObject(routerPath)
|
||||
.tint(self.tintColor)
|
||||
.preferredColorScheme(self.theme)
|
||||
.task {
|
||||
|
@ -63,7 +66,10 @@ struct VernissageApp: App {
|
|||
return
|
||||
}
|
||||
|
||||
self.applicationState.accountData = accountData
|
||||
Task { @MainActor in
|
||||
self.applicationState.accountData = accountData
|
||||
}
|
||||
|
||||
self.applicationViewMode = .mainView
|
||||
})
|
||||
}
|
||||
|
@ -74,6 +80,12 @@ struct VernissageApp: App {
|
|||
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)) { _ in
|
||||
HapticService.shared.stop()
|
||||
}
|
||||
.onChange(of: applicationState.theme) { newValue in
|
||||
self.theme = newValue.colorScheme()
|
||||
}
|
||||
.onChange(of: applicationState.tintColor) { newValue in
|
||||
self.tintColor = newValue.color()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,16 +28,16 @@ struct AccountsView: View {
|
|||
var body: some View {
|
||||
List {
|
||||
ForEach(accounts, id: \.id) { account in
|
||||
NavigationLink(destination: UserProfileView(
|
||||
NavigationLink(value: RouteurDestinations.userProfile(
|
||||
accountId: account.id,
|
||||
accountDisplayName: account.displayName,
|
||||
accountDisplayName: account.displayNameWithoutEmojis,
|
||||
accountUserName: account.acct)
|
||||
.environmentObject(applicationState)) {
|
||||
UsernameRow(accountId: account.id,
|
||||
accountAvatar: account.avatar,
|
||||
accountDisplayName: account.displayNameWithoutEmojis,
|
||||
accountUsername: account.acct)
|
||||
}
|
||||
) {
|
||||
UsernameRow(accountId: account.id,
|
||||
accountAvatar: account.avatar,
|
||||
accountDisplayName: account.displayNameWithoutEmojis,
|
||||
accountUsername: account.acct)
|
||||
}
|
||||
}
|
||||
|
||||
if allItemsLoaded == false && firstLoadFinished == true {
|
||||
|
|
|
@ -15,7 +15,7 @@ struct ComposeView: View {
|
|||
@EnvironmentObject var applicationState: ApplicationState
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@Binding var statusViewModel: StatusViewModel?
|
||||
@State var statusViewModel: StatusViewModel?
|
||||
@State private var text = String.empty()
|
||||
|
||||
@FocusState private var focusedField: FocusField?
|
||||
|
@ -59,9 +59,8 @@ struct ComposeView: View {
|
|||
Spacer()
|
||||
}
|
||||
|
||||
HTMLFormattedText(status.content, withFontSize: 14, andWidth: contentWidth)
|
||||
.padding(.top, -4)
|
||||
.padding(.leading, -4)
|
||||
MarkdownFormattedText(status.content.asMarkdown, withFontSize: 14, andWidth: contentWidth)
|
||||
.environment(\.openURL, OpenURLAction { url in .handled })
|
||||
}
|
||||
}
|
||||
.padding(8)
|
||||
|
|
|
@ -9,6 +9,7 @@ import SwiftUI
|
|||
struct HomeFeedView: View {
|
||||
@Environment(\.managedObjectContext) private var viewContext
|
||||
@EnvironmentObject var applicationState: ApplicationState
|
||||
@EnvironmentObject var routerPath: RouterPath
|
||||
|
||||
@State private var firstLoadFinished = false
|
||||
@State private var allItemsBottomLoaded = false
|
||||
|
@ -28,11 +29,12 @@ struct HomeFeedView: View {
|
|||
ScrollView {
|
||||
LazyVGrid(columns: gridColumns) {
|
||||
ForEach(dbStatuses, id: \.self) { item in
|
||||
NavigationLink(destination: StatusView(statusId: item.id,
|
||||
imageBlurhash: item.attachments().first?.blurhash,
|
||||
imageWidth: item.attachments().first?.metaImageWidth,
|
||||
imageHeight: item.attachments().first?.metaImageHeight)
|
||||
.environmentObject(applicationState)) {
|
||||
NavigationLink(value: RouteurDestinations.status(
|
||||
id: item.id,
|
||||
blurhash: item.attachments().first?.blurhash,
|
||||
metaImageWidth: item.attachments().first?.metaImageWidth,
|
||||
metaImageHeight: item.attachments().first?.metaImageHeight)
|
||||
) {
|
||||
ImageRow(statusData: item)
|
||||
}
|
||||
.buttonStyle(EmptyButtonStyle())
|
||||
|
|
|
@ -17,12 +17,7 @@ struct MainView: View {
|
|||
|
||||
@Environment(\.managedObjectContext) private var viewContext
|
||||
@EnvironmentObject var applicationState: ApplicationState
|
||||
|
||||
var onTintChange: ((TintColor) -> Void)?
|
||||
var onThemeChange: ((Theme) -> Void)?
|
||||
|
||||
@State private var showSettings = false
|
||||
@State private var sheet: Sheet?
|
||||
@EnvironmentObject var routerPath: RouterPath
|
||||
|
||||
@State private var navBarTitle: String = "Home"
|
||||
@State private var viewMode: ViewMode = .home {
|
||||
|
@ -41,18 +36,6 @@ struct MainView: View {
|
|||
self.getMainView()
|
||||
.navigationBarTitle(navBarTitle)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.sheet(item: $sheet, content: { item in
|
||||
switch item {
|
||||
case .settings:
|
||||
SettingsView { color in
|
||||
self.onTintChange?(color)
|
||||
} onThemeChange: { theme in
|
||||
self.onThemeChange?(theme)
|
||||
}
|
||||
case .compose:
|
||||
ComposeView(statusViewModel: .constant(nil))
|
||||
}
|
||||
})
|
||||
.toolbar {
|
||||
self.getLeadingToolbar()
|
||||
self.getPrincipalToolbar()
|
||||
|
@ -70,10 +53,10 @@ struct MainView: View {
|
|||
TrendStatusesView(accountId: applicationState.accountData?.id ?? String.empty())
|
||||
.id(applicationState.accountData?.id ?? String.empty())
|
||||
case .local:
|
||||
StatusesView(accountId: applicationState.accountData?.id ?? String.empty(), listType: .local)
|
||||
StatusesView(listType: .local)
|
||||
.id(applicationState.accountData?.id ?? String.empty())
|
||||
case .federated:
|
||||
StatusesView(accountId: applicationState.accountData?.id ?? String.empty(), listType: .federated)
|
||||
StatusesView(listType: .federated)
|
||||
.id(applicationState.accountData?.id ?? String.empty())
|
||||
case .profile:
|
||||
if let accountData = self.applicationState.accountData {
|
||||
|
@ -182,7 +165,7 @@ struct MainView: View {
|
|||
Divider()
|
||||
|
||||
Button {
|
||||
self.sheet = .settings
|
||||
self.routerPath.presentedSheet = .settings
|
||||
} label: {
|
||||
Label("Settings", systemImage: "gear")
|
||||
}
|
||||
|
@ -207,7 +190,7 @@ struct MainView: View {
|
|||
if viewMode == .local || viewMode == .home || viewMode == .federated {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button {
|
||||
self.sheet = .compose
|
||||
self.routerPath.presentedSheet = .newStatusEditor
|
||||
} label: {
|
||||
Image(systemName: "square.and.pencil")
|
||||
.tint(.mainTextColor)
|
||||
|
|
|
@ -25,29 +25,34 @@ struct NotificationsView: View {
|
|||
ForEach(notifications, id: \.id) { notification in
|
||||
switch notification.type {
|
||||
case .favourite, .reblog, .mention, .status, .poll, .update:
|
||||
if let status = notification.status {
|
||||
NavigationLink(destination: StatusView(statusId: status.id)
|
||||
.environmentObject(applicationState)) {
|
||||
NotificationRow(notification: notification)
|
||||
}
|
||||
if let status = notification.status, let statusViewModel = StatusViewModel(status: status) {
|
||||
NavigationLink(value: RouteurDestinations.status(
|
||||
id: statusViewModel.id,
|
||||
blurhash: statusViewModel.mediaAttachments.first?.blurhash,
|
||||
metaImageWidth: statusViewModel.getImageWidth(),
|
||||
metaImageHeight: statusViewModel.getImageHeight())
|
||||
) {
|
||||
NotificationRow(notification: notification)
|
||||
}
|
||||
.buttonStyle(EmptyButtonStyle())
|
||||
}
|
||||
case .follow, .followRequest, .adminSignUp:
|
||||
NavigationLink(destination: UserProfileView(
|
||||
NavigationLink(value: RouteurDestinations.userProfile(
|
||||
accountId: notification.account.id,
|
||||
accountDisplayName: notification.account.displayNameWithoutEmojis,
|
||||
accountUserName: notification.account.acct)
|
||||
.environmentObject(applicationState)) {
|
||||
NotificationRow(notification: notification)
|
||||
}
|
||||
) {
|
||||
NotificationRow(notification: notification)
|
||||
}
|
||||
case .adminReport:
|
||||
if let targetAccount = notification.report?.targetAccount {
|
||||
NavigationLink(destination: UserProfileView(
|
||||
NavigationLink(value: RouteurDestinations.userProfile(
|
||||
accountId: targetAccount.id,
|
||||
accountDisplayName: targetAccount.displayNameWithoutEmojis,
|
||||
accountUserName: targetAccount.acct)
|
||||
.environmentObject(applicationState)) {
|
||||
NotificationRow(notification: notification)
|
||||
}
|
||||
) {
|
||||
NotificationRow(notification: notification)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,55 +19,58 @@ struct SettingsView: View {
|
|||
var onThemeChange: ((Theme) -> Void)?
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
List {
|
||||
// Accounts.
|
||||
AccountsSection()
|
||||
|
||||
// Themes.
|
||||
ThemeSection { theme in
|
||||
changeTheme(theme: theme)
|
||||
}
|
||||
|
||||
// Accents.
|
||||
AccentsSection { color in
|
||||
self.onTintChange?(color)
|
||||
}
|
||||
|
||||
// Other.
|
||||
Section("Other") {
|
||||
Text("Third party") // Link to dependeinces
|
||||
Text("Report a bug")
|
||||
Text("Follow me on Mastodon")
|
||||
}
|
||||
|
||||
// Version.
|
||||
Section() {
|
||||
HStack {
|
||||
Text("Version")
|
||||
Spacer()
|
||||
Text("\(appVersion ?? String.empty()) (\(appBundleVersion ?? String.empty()))")
|
||||
.foregroundColor(.accentColor)
|
||||
NavigationStack {
|
||||
NavigationView {
|
||||
List {
|
||||
// Accounts.
|
||||
AccountsSection()
|
||||
|
||||
// Themes.
|
||||
ThemeSection { theme in
|
||||
changeTheme(theme: theme)
|
||||
}
|
||||
|
||||
// Accents.
|
||||
AccentsSection { color in
|
||||
self.onTintChange?(color)
|
||||
}
|
||||
|
||||
// Other.
|
||||
Section("Other") {
|
||||
Text("Third party") // Link to dependeinces
|
||||
Text("Report a bug")
|
||||
Text("Follow me on Mastodon")
|
||||
}
|
||||
|
||||
// Version.
|
||||
Section() {
|
||||
HStack {
|
||||
Text("Version")
|
||||
Spacer()
|
||||
Text("\(appVersion ?? String.empty()) (\(appBundleVersion ?? String.empty()))")
|
||||
.foregroundColor(.accentColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(alignment: .topLeading)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Close", role: .cancel) {
|
||||
dismiss()
|
||||
.frame(alignment: .topLeading)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Close", role: .cancel) {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
.task {
|
||||
self.appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
|
||||
self.appBundleVersion = Bundle.main.infoDictionary?["CFBundleVersion"] as? String
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification), perform: { _ in
|
||||
self.theme = applicationState.theme.colorScheme() ?? self.getSystemColorScheme()
|
||||
})
|
||||
.navigationBarTitle(Text("Settings"), displayMode: .inline)
|
||||
.preferredColorScheme(self.theme)
|
||||
}
|
||||
.task {
|
||||
self.appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
|
||||
self.appBundleVersion = Bundle.main.infoDictionary?["CFBundleVersion"] as? String
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification), perform: { _ in
|
||||
self.theme = applicationState.theme.colorScheme() ?? self.getSystemColorScheme()
|
||||
})
|
||||
.navigationBarTitle(Text("Settings"), displayMode: .inline)
|
||||
.preferredColorScheme(self.theme)
|
||||
.withAppRouteur()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -13,7 +13,6 @@ struct SignInView: View {
|
|||
|
||||
@State private var serverAddress: String = String.empty()
|
||||
|
||||
|
||||
var onSignInStateChenge: ((_ applicationViewMode: ApplicationViewMode) -> Void)?
|
||||
|
||||
var body: some View {
|
||||
|
|
|
@ -10,6 +10,8 @@ import AVFoundation
|
|||
|
||||
struct StatusView: View {
|
||||
@EnvironmentObject var applicationState: ApplicationState
|
||||
@EnvironmentObject var routerPath: RouterPath
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@State var statusId: String
|
||||
|
@ -17,8 +19,6 @@ struct StatusView: View {
|
|||
@State var imageWidth: Int32?
|
||||
@State var imageHeight: Int32?
|
||||
|
||||
@State private var messageForStatus: StatusViewModel?
|
||||
@State private var showCompose = false
|
||||
@State private var showImageViewer = false
|
||||
@State private var firstLoadFinished = false
|
||||
|
||||
|
@ -47,20 +47,22 @@ struct StatusView: View {
|
|||
}
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
NavigationLink(destination: UserProfileView(
|
||||
NavigationLink(value: RouteurDestinations.userProfile(
|
||||
accountId: statusViewModel.account.id,
|
||||
accountDisplayName: statusViewModel.account.displayName,
|
||||
accountUserName: statusViewModel.account.username)
|
||||
.environmentObject(applicationState)) {
|
||||
UsernameRow(accountId: statusViewModel.account.id,
|
||||
accountAvatar: statusViewModel.account.avatar,
|
||||
accountDisplayName: statusViewModel.account.displayNameWithoutEmojis,
|
||||
accountUsername: statusViewModel.account.username)
|
||||
}
|
||||
|
||||
HTMLFormattedText(statusViewModel.content)
|
||||
.padding(.leading, -4)
|
||||
accountDisplayName: statusViewModel.account.displayNameWithoutEmojis,
|
||||
accountUserName: statusViewModel.account.acct)
|
||||
) {
|
||||
UsernameRow(accountId: statusViewModel.account.id,
|
||||
accountAvatar: statusViewModel.account.avatar,
|
||||
accountDisplayName: statusViewModel.account.displayNameWithoutEmojis,
|
||||
accountUsername: statusViewModel.account.username)
|
||||
}
|
||||
|
||||
MarkdownFormattedText(statusViewModel.content.asMarkdown)
|
||||
.environment(\.openURL, OpenURLAction { url in
|
||||
routerPath.handle(url: url, accountData: self.applicationState.accountData)
|
||||
})
|
||||
|
||||
VStack (alignment: .leading) {
|
||||
if let name = statusViewModel.place?.name, let country = statusViewModel.place?.country {
|
||||
LabelIcon(iconName: "mappin.and.ellipse", value: "\(name), \(country)")
|
||||
|
@ -85,28 +87,19 @@ struct StatusView: View {
|
|||
.foregroundColor(.lightGrayColor)
|
||||
.font(.footnote)
|
||||
|
||||
InteractionRow(statusViewModel: statusViewModel) {
|
||||
self.messageForStatus = statusViewModel
|
||||
self.showCompose.toggle()
|
||||
}
|
||||
.foregroundColor(.accentColor)
|
||||
.padding(8)
|
||||
InteractionRow(statusViewModel: statusViewModel)
|
||||
.foregroundColor(.accentColor)
|
||||
.padding(8)
|
||||
}
|
||||
.padding(8)
|
||||
|
||||
CommentsSection(statusId: statusViewModel.id) { messageForStatus in
|
||||
self.messageForStatus = messageForStatus
|
||||
self.showCompose.toggle()
|
||||
}
|
||||
CommentsSection(statusId: statusViewModel.id)
|
||||
}
|
||||
} else {
|
||||
StatusPlaceholder(imageHeight: self.getImageHeight(), imageBlurhash: self.imageBlurhash)
|
||||
}
|
||||
}
|
||||
.navigationBarTitle("Details")
|
||||
.sheet(isPresented: $showCompose, content: {
|
||||
ComposeView(statusViewModel: $messageForStatus)
|
||||
})
|
||||
.fullScreenCover(isPresented: $showImageViewer, content: {
|
||||
if let statusViewModel = self.statusViewModel {
|
||||
ImagesViewer(statusViewModel: statusViewModel, selectedAttachmentId: selectedAttachmentId ?? String.empty())
|
||||
|
|
|
@ -8,15 +8,17 @@ import SwiftUI
|
|||
import MastodonKit
|
||||
|
||||
struct StatusesView: View {
|
||||
public enum ListType {
|
||||
public enum ListType: Hashable {
|
||||
case local
|
||||
case federated
|
||||
case favourites
|
||||
case bookmarks
|
||||
case hashtag(tag: String)
|
||||
}
|
||||
|
||||
@EnvironmentObject private var applicationState: ApplicationState
|
||||
@State public var accountId: String
|
||||
@EnvironmentObject private var routerPath: RouterPath
|
||||
|
||||
@State public var listType: ListType
|
||||
|
||||
@State private var allItemsLoaded = false
|
||||
|
@ -30,15 +32,15 @@ struct StatusesView: View {
|
|||
VStack(alignment: .center) {
|
||||
if firstLoadFinished == true {
|
||||
ForEach(self.statusViewModels, id: \.uniqueId) { item in
|
||||
NavigationLink(destination: StatusView(statusId: item.id,
|
||||
imageBlurhash: item.mediaAttachments.first?.blurhash,
|
||||
imageWidth: item.getImageWidth(),
|
||||
imageHeight: item.getImageHeight())
|
||||
.environmentObject(applicationState)) {
|
||||
ImageRowAsync(statusViewModel: item)
|
||||
}
|
||||
.buttonStyle(EmptyButtonStyle())
|
||||
|
||||
NavigationLink(value: RouteurDestinations.status(
|
||||
id: item.id,
|
||||
blurhash: item.mediaAttachments.first?.blurhash,
|
||||
metaImageWidth: item.getImageWidth(),
|
||||
metaImageHeight: item.getImageHeight())
|
||||
) {
|
||||
ImageRowAsync(statusViewModel: item)
|
||||
}
|
||||
.buttonStyle(EmptyButtonStyle())
|
||||
}
|
||||
|
||||
LazyVStack {
|
||||
|
@ -176,6 +178,16 @@ struct StatusesView: View {
|
|||
sinceId: sinceId,
|
||||
minId: minId,
|
||||
limit: self.defaultLimit)
|
||||
case .hashtag(let tag):
|
||||
return try await PublicTimelineService.shared.getTagStatuses(
|
||||
accountData: self.applicationState.accountData,
|
||||
tag: tag,
|
||||
local: false,
|
||||
remote: true,
|
||||
maxId: maxId,
|
||||
sinceId: sinceId,
|
||||
minId: minId,
|
||||
limit: self.defaultLimit)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -189,6 +201,8 @@ struct StatusesView: View {
|
|||
return "Favourites"
|
||||
case .bookmarks:
|
||||
return "Bookmarks"
|
||||
case .hashtag(let tag):
|
||||
return "#\(tag)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,15 +43,15 @@ struct TrendStatusesView: View {
|
|||
VStack(alignment: .center) {
|
||||
if firstLoadFinished == true {
|
||||
ForEach(self.statusViewModels, id: \.uniqueId) { item in
|
||||
NavigationLink(destination: StatusView(statusId: item.id,
|
||||
imageBlurhash: item.mediaAttachments.first?.blurhash,
|
||||
imageWidth: item.getImageWidth(),
|
||||
imageHeight: item.getImageHeight())
|
||||
.environmentObject(applicationState)) {
|
||||
ImageRowAsync(statusViewModel: item)
|
||||
}
|
||||
.buttonStyle(EmptyButtonStyle())
|
||||
|
||||
NavigationLink(value: RouteurDestinations.status(
|
||||
id: item.id,
|
||||
blurhash: item.mediaAttachments.first?.blurhash,
|
||||
metaImageWidth: item.getImageWidth(),
|
||||
metaImageHeight: item.getImageHeight())
|
||||
) {
|
||||
ImageRowAsync(statusViewModel: item)
|
||||
}
|
||||
.buttonStyle(EmptyButtonStyle())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -127,17 +127,14 @@ struct UserProfileView: View {
|
|||
Divider()
|
||||
}
|
||||
|
||||
NavigationLink(destination: StatusesView(accountId: applicationState.accountData?.id ?? String.empty(), listType: .favourites)
|
||||
.environmentObject(applicationState)
|
||||
) {
|
||||
NavigationLink(value: RouteurDestinations.statuses(listType: .favourites)) {
|
||||
Label("Favourites", systemImage: "hand.thumbsup")
|
||||
}
|
||||
|
||||
NavigationLink(destination: StatusesView(accountId: applicationState.accountData?.id ?? String.empty(), listType: .bookmarks)
|
||||
.environmentObject(applicationState)
|
||||
) {
|
||||
NavigationLink(value: RouteurDestinations.statuses(listType: .bookmarks)) {
|
||||
Label("Bookmarks", systemImage: "bookmark")
|
||||
}
|
||||
|
||||
}, label: {
|
||||
Image(systemName: "gear")
|
||||
.tint(.mainTextColor)
|
||||
|
|
|
@ -10,6 +10,7 @@ import Drops
|
|||
|
||||
struct InteractionRow: View {
|
||||
@EnvironmentObject var applicationState: ApplicationState
|
||||
@EnvironmentObject var routerPath: RouterPath
|
||||
|
||||
@State var statusViewModel: StatusViewModel
|
||||
|
||||
|
@ -19,13 +20,11 @@ struct InteractionRow: View {
|
|||
@State private var favourited = false
|
||||
@State private var favouritesCount = 0
|
||||
@State private var bookmarked = false
|
||||
|
||||
var onNewStatus: (() -> Void)?
|
||||
|
||||
|
||||
var body: some View {
|
||||
HStack (alignment: .top) {
|
||||
ActionButton {
|
||||
onNewStatus?()
|
||||
self.routerPath.presentedSheet = .replyToStatusEditor(status: statusViewModel)
|
||||
} label: {
|
||||
HStack(alignment: .center) {
|
||||
Image(systemName: "message")
|
||||
|
@ -111,15 +110,11 @@ struct InteractionRow: View {
|
|||
Spacer()
|
||||
|
||||
Menu {
|
||||
NavigationLink(destination: AccountsView(entityId: statusViewModel.id, listType: .reblogged)
|
||||
.environmentObject(applicationState)
|
||||
) {
|
||||
NavigationLink(value: RouteurDestinations.accounts(entityId: statusViewModel.id, listType: .reblogged)) {
|
||||
Label("Reboosted by", systemImage: "paperplane")
|
||||
}
|
||||
|
||||
NavigationLink(destination: AccountsView(entityId: statusViewModel.id, listType: .favourited)
|
||||
.environmentObject(applicationState)
|
||||
) {
|
||||
|
||||
NavigationLink(value: RouteurDestinations.accounts(entityId: statusViewModel.id, listType: .favourited)) {
|
||||
Label("Favourited by", systemImage: "hand.thumbsup")
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
//
|
||||
// https://mczachurski.dev
|
||||
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
import EmojiText
|
||||
import HTML2Markdown
|
||||
|
||||
struct MarkdownFormattedText: View {
|
||||
@EnvironmentObject var applicationState: ApplicationState
|
||||
|
||||
private let markdown: String
|
||||
private let textView = UITextView()
|
||||
private let fontSize: CGFloat
|
||||
private let width: Int
|
||||
|
||||
init(_ markdown: String, withFontSize fontSize: CGFloat = 16, andWidth width: Int? = nil) {
|
||||
self.markdown = markdown
|
||||
self.fontSize = fontSize
|
||||
self.width = width ?? Int(UIScreen.main.bounds.width) - 16
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
EmojiText(markdown: markdown, emojis: [])
|
||||
.font(.system(size: self.fontSize))
|
||||
}
|
||||
}
|
|
@ -69,9 +69,8 @@ struct NotificationRow: View {
|
|||
}
|
||||
case .follow, .followRequest, .adminSignUp:
|
||||
if let note = self.notification.account.note {
|
||||
HTMLFormattedText(note, withFontSize: 12, andWidth: contentWidth)
|
||||
.padding(.top, -4)
|
||||
.padding(.leading, -4)
|
||||
MarkdownFormattedText(note.asMarkdown, withFontSize: 12, andWidth: contentWidth)
|
||||
.environment(\.openURL, OpenURLAction { url in .handled })
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ struct AccountsSection: View {
|
|||
accountUsername: account.username)
|
||||
}
|
||||
|
||||
NavigationLink(destination: SignInView()) {
|
||||
NavigationLink(value: RouteurDestinations.signIn) {
|
||||
HStack {
|
||||
Text("New account")
|
||||
Spacer()
|
||||
|
|
|
@ -16,13 +16,13 @@ struct CommentBody: View {
|
|||
var body: some View {
|
||||
HStack (alignment: .top) {
|
||||
|
||||
NavigationLink(destination: UserProfileView(
|
||||
NavigationLink(value: RouteurDestinations.userProfile(
|
||||
accountId: self.statusViewModel.account.id,
|
||||
accountDisplayName: self.statusViewModel.account.displayName,
|
||||
accountUserName: self.statusViewModel.account.acct)
|
||||
.environmentObject(applicationState)) {
|
||||
UserAvatar(accountId: self.statusViewModel.account.id, accountAvatar: self.statusViewModel.account.avatar, width: 32, height: 32)
|
||||
}
|
||||
) {
|
||||
UserAvatar(accountId: self.statusViewModel.account.id, accountAvatar: self.statusViewModel.account.avatar, width: 32, height: 32)
|
||||
}
|
||||
|
||||
VStack (alignment: .leading, spacing: 0) {
|
||||
HStack (alignment: .top) {
|
||||
|
@ -38,9 +38,9 @@ struct CommentBody: View {
|
|||
.font(.footnote)
|
||||
}
|
||||
|
||||
HTMLFormattedText(self.statusViewModel.content, withFontSize: 14, andWidth: contentWidth)
|
||||
.padding(.top, -4)
|
||||
.padding(.leading, -4)
|
||||
MarkdownFormattedText(self.statusViewModel.content.asMarkdown, withFontSize: 14, andWidth: contentWidth)
|
||||
.environment(\.openURL, OpenURLAction { url in .handled })
|
||||
.padding(.top, 4)
|
||||
|
||||
if self.statusViewModel.mediaAttachments.count > 0 {
|
||||
LazyVGrid(
|
||||
|
|
|
@ -11,9 +11,7 @@ struct CommentsSection: View {
|
|||
@Environment(\.colorScheme) var colorScheme
|
||||
@EnvironmentObject var applicationState: ApplicationState
|
||||
|
||||
@State public var statusId: String
|
||||
var onNewStatus: ((_ context: StatusViewModel) -> Void)?
|
||||
|
||||
@State public var statusId: String
|
||||
@State private var commentViewModels: [CommentViewModel]?
|
||||
|
||||
var body: some View {
|
||||
|
@ -33,12 +31,10 @@ struct CommentsSection: View {
|
|||
|
||||
if self.applicationState.showInteractionStatusId == commentViewModel.status.id {
|
||||
VStack (alignment: .leading, spacing: 0) {
|
||||
InteractionRow(statusViewModel: commentViewModel.status) {
|
||||
self.onNewStatus?(commentViewModel.status)
|
||||
}
|
||||
.foregroundColor(self.getInteractionRowTextColor())
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 8)
|
||||
InteractionRow(statusViewModel: commentViewModel.status)
|
||||
.foregroundColor(self.getInteractionRowTextColor())
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
.background(Color.lightGrayColor.opacity(0.5))
|
||||
.transition(AnyTransition.move(edge: .top).combined(with: .opacity))
|
||||
|
|
|
@ -9,6 +9,8 @@ import MastodonKit
|
|||
|
||||
struct UserProfileHeader: View {
|
||||
@EnvironmentObject private var applicationState: ApplicationState
|
||||
@EnvironmentObject private var routerPath: RouterPath
|
||||
|
||||
@State var account: Account
|
||||
@State var relationship: Relationship? = nil
|
||||
|
||||
|
@ -29,9 +31,7 @@ struct UserProfileHeader: View {
|
|||
|
||||
Spacer()
|
||||
|
||||
NavigationLink(destination: AccountsView(entityId: account.id, listType: .followers)
|
||||
.environmentObject(applicationState)
|
||||
) {
|
||||
NavigationLink(value: RouteurDestinations.accounts(entityId: account.id, listType: .followers)) {
|
||||
VStack(alignment: .center) {
|
||||
Text("\(account.followersCount)")
|
||||
.font(.title3)
|
||||
|
@ -40,12 +40,10 @@ struct UserProfileHeader: View {
|
|||
.opacity(0.6)
|
||||
}
|
||||
}.foregroundColor(.mainTextColor)
|
||||
|
||||
|
||||
Spacer()
|
||||
|
||||
NavigationLink(destination: AccountsView(entityId: account.id, listType: .following)
|
||||
.environmentObject(applicationState)
|
||||
) {
|
||||
NavigationLink(value: RouteurDestinations.accounts(entityId: account.id, listType: .following)) {
|
||||
VStack(alignment: .center) {
|
||||
Text("\(account.followingCount)")
|
||||
.font(.title3)
|
||||
|
@ -75,9 +73,10 @@ struct UserProfileHeader: View {
|
|||
}
|
||||
|
||||
if let note = account.note, !note.isEmpty {
|
||||
HTMLFormattedText(note, withFontSize: 14, andWidth: Int(UIScreen.main.bounds.width) - 16)
|
||||
.padding(.top, -10)
|
||||
.padding(.leading, -4)
|
||||
MarkdownFormattedText(note.asMarkdown, withFontSize: 14, andWidth: Int(UIScreen.main.bounds.width) - 16)
|
||||
.environment(\.openURL, OpenURLAction { url in
|
||||
routerPath.handle(url: url, accountData: self.applicationState.accountData)
|
||||
})
|
||||
}
|
||||
|
||||
Text("Joined \(account.createdAt.toRelative(.isoDateTimeMilliSec))")
|
||||
|
|
|
@ -21,15 +21,15 @@ struct UserProfileStatuses: View {
|
|||
VStack(alignment: .center) {
|
||||
if firstLoadFinished == true {
|
||||
ForEach(self.statusViewModels, id: \.uniqueId) { item in
|
||||
NavigationLink(destination: StatusView(statusId: item.id,
|
||||
imageBlurhash: item.mediaAttachments.first?.blurhash,
|
||||
imageWidth: item.getImageWidth(),
|
||||
imageHeight: item.getImageHeight())
|
||||
.environmentObject(applicationState)) {
|
||||
ImageRowAsync(statusViewModel: item)
|
||||
}
|
||||
.buttonStyle(EmptyButtonStyle())
|
||||
|
||||
NavigationLink(value: RouteurDestinations.status(
|
||||
id: item.id,
|
||||
blurhash: item.mediaAttachments.first?.blurhash,
|
||||
metaImageWidth: item.getImageWidth(),
|
||||
metaImageHeight: item.getImageHeight())
|
||||
) {
|
||||
ImageRowAsync(statusViewModel: item)
|
||||
}
|
||||
.buttonStyle(EmptyButtonStyle())
|
||||
}
|
||||
|
||||
LazyVStack {
|
||||
|
|
Loading…
Reference in New Issue