Add hashtags lists.

This commit is contained in:
Marcin Czachursk 2023-01-23 18:01:27 +01:00
parent 9dcbd5336b
commit 5736734695
31 changed files with 517 additions and 314 deletions

View File

@ -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 {

View File

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

View File

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

View File

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

View File

@ -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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 {

View File

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

View File

@ -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())

View File

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

View File

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

View File

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

View File

@ -13,7 +13,6 @@ struct SignInView: View {
@State private var serverAddress: String = String.empty()
var onSignInStateChenge: ((_ applicationViewMode: ApplicationViewMode) -> Void)?
var body: some View {

View File

@ -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())

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,7 +18,7 @@ struct AccountsSection: View {
accountUsername: account.username)
}
NavigationLink(destination: SignInView()) {
NavigationLink(value: RouteurDestinations.signIn) {
HStack {
Text("New account")
Spacer()

View File

@ -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(

View File

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

View File

@ -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))")

View File

@ -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 {