Change details view

This commit is contained in:
Marcin Czachursk 2023-01-04 17:56:01 +01:00
parent 89b2bdee98
commit 8bb6b1d985
14 changed files with 288 additions and 257 deletions

View File

@ -15,7 +15,6 @@
F800480A2961EA1900E6868A /* AttachmentDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80048092961EA1900E6868A /* AttachmentDataHandler.swift */; };
F8341F90295C636C009C8EE6 /* UIImage+Exif.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8341F8F295C636C009C8EE6 /* UIImage+Exif.swift */; };
F8341F92295C63BB009C8EE6 /* ImageStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8341F91295C63BB009C8EE6 /* ImageStatus.swift */; };
F83901A4295D864D00456AE2 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = F83901A3295D864D00456AE2 /* Tag.swift */; };
F83901A6295D8EC000456AE2 /* LabelIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = F83901A5295D8EC000456AE2 /* LabelIcon.swift */; };
F85D4971296402DC00751DF7 /* AuthorizationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D4970296402DC00751DF7 /* AuthorizationService.swift */; };
F85D4973296406E700751DF7 /* BottomRight.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D4972296406E700751DF7 /* BottomRight.swift */; };
@ -64,7 +63,6 @@
F80048092961EA1900E6868A /* AttachmentDataHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentDataHandler.swift; sourceTree = "<group>"; };
F8341F8F295C636C009C8EE6 /* UIImage+Exif.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Exif.swift"; sourceTree = "<group>"; };
F8341F91295C63BB009C8EE6 /* ImageStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageStatus.swift; sourceTree = "<group>"; };
F83901A3295D864D00456AE2 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = "<group>"; };
F83901A5295D8EC000456AE2 /* LabelIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelIcon.swift; sourceTree = "<group>"; };
F85D4970296402DC00751DF7 /* AuthorizationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizationService.swift; sourceTree = "<group>"; };
F85D4972296406E700751DF7 /* BottomRight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomRight.swift; sourceTree = "<group>"; };
@ -184,7 +182,6 @@
F83901A2295D863B00456AE2 /* Widgets */ = {
isa = PBXGroup;
children = (
F83901A3295D864D00456AE2 /* Tag.swift */,
F83901A5295D8EC000456AE2 /* LabelIcon.swift */,
F85D4972296406E700751DF7 /* BottomRight.swift */,
F85D497629640A5200751DF7 /* ImageRow.swift */,
@ -360,7 +357,6 @@
F866F6A729604629002E8F88 /* SignInView.swift in Sources */,
F88C246C295C37B80006098B /* VernissageApp.swift in Sources */,
F85D4971296402DC00751DF7 /* AuthorizationService.swift in Sources */,
F83901A4295D864D00456AE2 /* Tag.swift in Sources */,
F88FAD25295F3FF7009B20C9 /* FederatedFeedView.swift in Sources */,
F88FAD32295F5029009B20C9 /* RemoteFileService.swift in Sources */,
F88FAD27295F400E009B20C9 /* NotificationsView.swift in Sources */,

View File

@ -5,9 +5,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "0.561",
"red" : "0.000"
"blue" : "247",
"green" : "131",
"red" : "52"
}
},
"idiom" : "universal"
@ -23,9 +23,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "1.000",
"green" : "0.643",
"red" : "0.000"
"blue" : "248",
"green" : "167",
"red" : "74"
}
},
"idiom" : "universal"

View File

@ -1,38 +0,0 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "240",
"green" : "240",
"red" : "240"
}
},
"idiom" : "universal"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "30",
"green" : "30",
"red" : "30"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -5,9 +5,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0",
"green" : "0",
"red" : "0"
"blue" : "10",
"green" : "10",
"red" : "10"
}
},
"idiom" : "universal"
@ -23,9 +23,9 @@
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "255",
"green" : "255",
"red" : "255"
"blue" : "245",
"green" : "245",
"red" : "245"
}
},
"idiom" : "universal"

View File

@ -93,12 +93,19 @@ extension String {
case .emailTimePreview: return "dd MMM yyyy, h:mm a"
}
}
}
}
func toDate(_ format: DateFormatType = .isoDate) -> Date?{
func toDate(_ format: DateFormatType = .isoDate) -> Date? {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = format.stringFormat
let date = dateFormatter.date(from: self)
return date
}
func toRelative(_ format: DateFormatType = .isoDate) -> String {
let formatter = RelativeDateTimeFormatter()
let date = self.toDate(format) ?? Date()
return formatter.localizedString(for: date, relativeTo: Date.now)
}
}

View File

@ -26,6 +26,7 @@ struct HTMLFormattedText: UIViewRepresentable {
textView.isUserInteractionEnabled = false
textView.translatesAutoresizingMaskIntoConstraints = false
textView.isScrollEnabled = false
textView.backgroundColor = UIColor(Color.clear)
return textView
}
@ -47,12 +48,12 @@ struct HTMLFormattedText: UIViewRepresentable {
let largeAttributes = [
NSAttributedString.Key.font: UIFont.systemFont(ofSize: CGFloat(self.fontSize)),
NSAttributedString.Key.foregroundColor: UIColor(Color("mainTextColor"))
NSAttributedString.Key.foregroundColor: UIColor(Color("MainTextColor"))
]
let linkAttributes = [
NSAttributedString.Key.font: UIFont.systemFont(ofSize: CGFloat(self.fontSize)),
NSAttributedString.Key.foregroundColor: UIColor(Color("AccentColor"))
NSAttributedString.Key.foregroundColor: UIColor(Color.accentColor)
]
if let attributedString = try? NSMutableAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil) {

View File

@ -37,6 +37,34 @@ public class TimelineService {
try await self.loadData(for: accountData, on: backgroundContext, minId: newestStatus?.id)
}
public func getStatus(withId statusId: String, and accountData: AccountData) async throws -> Status? {
guard let accessToken = accountData.accessToken else {
return nil
}
let client = MastodonClient(baseURL: accountData.serverUrl).getAuthenticated(token: accessToken)
return try await client.read(statusId: statusId)
}
public func updateStatus(statusData: StatusData, and accountData: AccountData) async throws -> StatusData? {
guard let accessToken = accountData.accessToken else {
return nil
}
// Load data from API and operate on CoreData on background context.
let backgroundContext = CoreDataHandler.shared.newBackgroundContext()
// Get new information from API.
let client = MastodonClient(baseURL: accountData.serverUrl).getAuthenticated(token: accessToken)
let status = try await client.read(statusId: statusData.id)
// Update status data in database.
try await self.updateStatusData(from: status, to: statusData, on: backgroundContext)
try backgroundContext.save()
return statusData
}
public func getComments(for statusId: String, and accountData: AccountData) async throws -> Context {
let client = MastodonClient(baseURL: accountData.serverUrl).getAuthenticated(token: accountData.accessToken ?? "")
return try await client.getContext(for: statusId)
@ -47,43 +75,49 @@ public class TimelineService {
return
}
// Get maximimum downloaded stauts id.
let attachmentDataHandler = AttachmentDataHandler()
let statusDataHandler = StatusDataHandler()
// Retrieve statuses from API.
let client = MastodonClient(baseURL: accountData.serverUrl).getAuthenticated(token: accessToken)
let statuses = try await client.getHomeTimeline(maxId: maxId, minId: minId, limit: 40)
// Download status images and save it into database.
for status in statuses {
// Create handler for managing statuses in database.
let statusDataHandler = StatusDataHandler()
// Save status data in database.
let statusDataEntity = statusDataHandler.createStatusDataEntity(viewContext: backgroundContext)
statusDataEntity.accountAvatar = status.account?.avatar
statusDataEntity.accountDisplayName = status.account?.displayName
statusDataEntity.accountId = status.account!.id
statusDataEntity.accountUsername = status.account!.username
statusDataEntity.applicationName = status.application?.name
statusDataEntity.applicationWebsite = status.application?.website
statusDataEntity.bookmarked = status.bookmarked
statusDataEntity.content = status.content
statusDataEntity.createdAt = status.createdAt
statusDataEntity.favourited = status.favourited
statusDataEntity.favouritesCount = Int32(status.favouritesCount)
statusDataEntity.id = status.id
statusDataEntity.inReplyToAccount = status.inReplyToAccount
statusDataEntity.inReplyToId = status.inReplyToId
statusDataEntity.muted = status.muted
statusDataEntity.pinned = status.pinned
statusDataEntity.reblogged = status.reblogged
statusDataEntity.reblogsCount = Int32(status.reblogsCount)
statusDataEntity.repliesCount = Int32(status.repliesCount)
statusDataEntity.sensitive = status.sensitive
statusDataEntity.spoilerText = status.spoilerText
statusDataEntity.uri = status.uri
statusDataEntity.url = status.url
statusDataEntity.visibility = status.visibility.rawValue
for status in statuses {
let statusData = statusDataHandler.createStatusDataEntity(viewContext: backgroundContext)
try await self.updateStatusData(from: status, to: statusData, on: backgroundContext)
}
try backgroundContext.save()
}
private func updateStatusData(from status: Status, to statusData: StatusData, on backgroundContext: NSManagedObjectContext) async throws {
statusData.id = status.id
statusData.createdAt = status.createdAt
statusData.accountAvatar = status.account?.avatar
statusData.accountDisplayName = status.account?.displayName
statusData.accountId = status.account!.id
statusData.accountUsername = status.account!.username
statusData.applicationName = status.application?.name
statusData.applicationWebsite = status.application?.website
statusData.bookmarked = status.bookmarked
statusData.content = status.content
statusData.favourited = status.favourited
statusData.favouritesCount = Int32(status.favouritesCount)
statusData.inReplyToAccount = status.inReplyToAccount
statusData.inReplyToId = status.inReplyToId
statusData.muted = status.muted
statusData.pinned = status.pinned
statusData.reblogged = status.reblogged
statusData.reblogsCount = Int32(status.reblogsCount)
statusData.repliesCount = Int32(status.repliesCount)
statusData.sensitive = status.sensitive
statusData.spoilerText = status.spoilerText
statusData.uri = status.uri
statusData.url = status.url
statusData.visibility = status.visibility.rawValue
let attachmentDataHandler = AttachmentDataHandler()
for attachment in status.mediaAttachments {
let imageData = try await self.fetchImage(attachment: attachment)
@ -103,7 +137,9 @@ public class TimelineService {
*/
// Save attachment in database.
let attachmentData = attachmentDataHandler.createAttachmnentDataEntity(viewContext: backgroundContext)
let attachmentData = statusData.attachments().first { item in item.id == attachment.id }
?? attachmentDataHandler.createAttachmnentDataEntity(viewContext: backgroundContext)
attachmentData.id = attachment.id
attachmentData.url = attachment.url
attachmentData.blurhash = attachment.blurhash
@ -112,15 +148,14 @@ public class TimelineService {
attachmentData.text = attachment.description
attachmentData.type = attachment.type.rawValue
attachmentData.statusId = statusDataEntity.id
attachmentData.statusId = statusData.id
attachmentData.data = imageData
attachmentData.statusRelation = statusDataEntity
statusDataEntity.addToAttachmentRelation(attachmentData)
if attachmentData.isInserted {
attachmentData.statusRelation = statusData
statusData.addToAttachmentRelation(attachmentData)
}
}
try backgroundContext.save()
}
private func fetchImage(attachment: Attachment) async throws -> Data? {

View File

@ -9,7 +9,8 @@ import MastodonSwift
import AVFoundation
struct DetailsView: View {
@State public var statusData: StatusData
@EnvironmentObject var applicationState: ApplicationState
@ObservedObject public var statusData: StatusData
var body: some View {
ScrollView {
@ -18,6 +19,7 @@ struct DetailsView: View {
VStack(alignment: .leading) {
UsernameRow(statusData: statusData)
HTMLFormattedText(statusData.content)
.padding(.leading, -4)
@ -27,44 +29,44 @@ struct DetailsView: View {
LabelIcon(iconName: "timelapse", value: "24.0 mm, f/1.8, 1/640s, ISO 100")
LabelIcon(iconName: "calendar", value: "2 Oct 2022")
}
.foregroundColor(Color("lightGrayColor"))
.foregroundColor(Color("LightGrayColor"))
HStack {
Text("Uploaded")
Text(statusData.createdAt.toDate(.isoDateTimeMilliSec) ?? Date(), style: .relative)
Text(statusData.createdAt.toRelative(.isoDateTimeMilliSec))
.padding(.horizontal, -4)
Text("ago")
if let applicationName = statusData.applicationName {
Text("via \(applicationName)")
.padding(.horizontal, -4)
}
}
.foregroundColor(Color("lightGrayColor"))
.foregroundColor(Color("LightGrayColor"))
.font(.footnote)
InteractionRow(statusData: statusData)
.padding(8)
}
.padding(8)
if statusData.repliesCount > 0 {
HStack (alignment: .center) {
Image(systemName: "message")
.padding(.leading, 8)
.padding(.vertical, 8)
Text("\(statusData.repliesCount) replies")
Spacer()
}
.font(.footnote)
.frame(maxWidth: .infinity)
.background(Color("mainTextColor").opacity(0.05))
.foregroundColor(Color("lightGrayColor"))
Rectangle()
.size(width: UIScreen.main.bounds.width, height: 4)
.fill(Color("MainTextColor"))
.opacity(0.1)
CommentsSection(statusId: statusData.id)
}
}
}
.navigationBarTitle("Details")
.onAppear {
Task {
do {
if let accountData = self.applicationState.accountData {
let timelineService = TimelineService()
_ = try await timelineService.updateStatus(statusData: self.statusData, and: accountData)
}
} catch {
print("Error \(error.localizedDescription)")
}
}
}
}
}

View File

@ -97,7 +97,7 @@ struct MainView: View {
.font(.subheadline)
}
.frame(width: 150)
.foregroundColor(Color("mainTextColor"))
.foregroundColor(Color("MainTextColor"))
}
}
}
@ -114,7 +114,7 @@ struct MainView: View {
Text(self.applicationState.accountData?.displayName ?? self.applicationState.accountData?.username ?? "")
Image(systemName: "person.circle.fill")
.resizable()
.foregroundColor(Color("mainTextColor"))
.foregroundColor(Color("MainTextColor"))
}
}
@ -138,7 +138,7 @@ struct MainView: View {
Image(systemName: "person.circle")
.resizable()
.frame(width: 32.0, height: 32.0)
.foregroundColor(Color("mainTextColor"))
.foregroundColor(Color("MainTextColor"))
}
}
}

View File

@ -29,42 +29,87 @@ struct CommentsSection: View {
} placeholder: {
Image(systemName: "person.circle")
.resizable()
.foregroundColor(Color("mainTextColor"))
.foregroundColor(Color("MainTextColor"))
}
.frame(width: 32.0, height: 32.0)
VStack (alignment: .leading) {
HStack (alignment: .top) {
Text(status.account?.displayName ?? status.account?.username ?? "")
.foregroundColor(Color("displayNameColor"))
.foregroundColor(Color("DisplayNameColor"))
.font(.footnote)
.fontWeight(.bold)
Text("@\(status.account?.username ?? "")")
.foregroundColor(Color("lightGrayColor"))
.foregroundColor(Color("LightGrayColor"))
.font(.footnote)
Spacer()
Text(status.createdAt.toRelative(.isoDateTimeMilliSec))
.foregroundColor(Color("LightGrayColor").opacity(0.5))
.font(.footnote)
/*
Image(systemName: "message")
.foregroundColor(Color.accentColor)
Image(systemName: "hand.thumbsup")
.foregroundColor(Color.accentColor)
*/
}
.padding(.bottom, -10)
HTMLFormattedText(status.content, withFontSize: 14, andWidth: contentWidth)
.padding(.leading, -4)
if status.mediaAttachments.count > 0 {
LazyVGrid(columns: Array(repeating: .init(.flexible()), count: status.mediaAttachments.count == 1 ? 1 : 2), alignment: .center, spacing: 4) {
ForEach(status.mediaAttachments, id: \.id) { attachment in
AsyncImage(url: status.mediaAttachments[0].url) { image in
image
.resizable()
.scaledToFill()
.frame(minWidth: 0, maxWidth: .infinity)
.frame(height: status.mediaAttachments.count == 1 ? 200 : 100)
.cornerRadius(10)
.shadow(color: Color("MainTextColor").opacity(0.3), radius: 2)
} placeholder: {
Image(systemName: "photo")
.resizable()
.scaledToFit()
.frame(minWidth: 0, maxWidth: .infinity)
.frame(height: status.mediaAttachments.count == 1 ? 200 : 100)
.foregroundColor(Color("MainTextColor"))
.opacity(0.05)
}
}
}
.padding(.bottom, 8)
}
}
}
.padding(.horizontal, 8)
.padding(.bottom, 8)
CommentsSection(statusId: status.id, withDivider: false)
if withDivider {
Rectangle()
.size(width: UIScreen.main.bounds.width, height: 4)
.fill(Color("mainTextColor"))
.opacity(0.05)
.fill(Color("MainTextColor"))
.opacity(0.1)
}
}
}
}.task {
}
.task {
do {
if let accountData = applicationState.accountData {
self.context = try await TimelineService.shared.getComments(for: statusId, and: accountData)
self.context = try await TimelineService.shared.getComments(
for: statusId,
and: accountData)
}
} catch {
print("Error \(error.localizedDescription)")

View File

@ -7,38 +7,63 @@
import SwiftUI
struct InteractionRow: View {
@State public var statusData: StatusData
@ObservedObject public var statusData: StatusData
var body: some View {
HStack (alignment: .top) {
Tag {
// Favorite
} content: {
HStack {
Image(systemName: statusData.favourited ? "heart.fill" : "heart")
Text("\(statusData.favouritesCount) likes")
}
}
Tag {
// Reboost
} content: {
HStack {
Image(systemName: statusData.reblogged ? "arrowshape.turn.up.forward.fill" : "arrowshape.turn.up.forward")
Text("\(statusData.reblogsCount) boosts")
Button {
// Reply
} label: {
HStack(alignment: .center) {
Image(systemName: "message")
Text("\(statusData.repliesCount)")
.font(.caption)
}
}
Spacer()
Tag {
Button {
// Reboost
} label: {
HStack(alignment: .center) {
Image(systemName: statusData.reblogged ? "arrowshape.turn.up.forward.fill" : "arrowshape.turn.up.forward")
Text("\(statusData.reblogsCount)")
.font(.caption)
}
}
Spacer()
Button {
// Favorite
} label: {
HStack(alignment: .center) {
Image(systemName: statusData.favourited ? "hand.thumbsup.fill" : "hand.thumbsup")
Text("\(statusData.favouritesCount)")
.font(.caption)
}
}
Spacer()
Button {
// Bookmark
} content: {
} label: {
Image(systemName: statusData.bookmarked ? "bookmark.fill" : "bookmark")
}
Spacer()
Button {
// Share
} label: {
Image(systemName: "square.and.arrow.up")
}
.font(.subheadline)
.foregroundColor(Color("mainTextColor"))
}
.font(.title3)
.fontWeight(.semibold)
.foregroundColor(Color.accentColor)
}
}

View File

@ -11,13 +11,13 @@ struct LabelIcon: View {
let value: String
var body: some View {
HStack {
HStack(alignment: .center) {
Image(systemName: iconName)
.frame(width: 36)
.frame(width: 30, alignment: .leading)
Text(value)
.font(.footnote)
}
.padding(.vertical, 4)
.padding(.vertical, 2)
}
}

View File

@ -1,42 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2022 Marcin Czachurski and the repository contributors.
// Licensed under the MIT License.
//
import SwiftUI
struct Tag<Content: View>: View {
let content: Content
let action: () -> Void
init(action: @escaping () -> Void, @ViewBuilder content: () -> Content) {
self.content = content()
self.action = action
}
var body: some View {
Button {
self.action()
} label: {
content
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(Color("actionButtonColor"))
.clipShape(Capsule())
}
}
}
struct TagView_Previews: PreviewProvider {
static var previews: some View {
Tag {
} content: {
HStack {
Image(systemName: "arrow.2.squarepath")
Text("7 boosts")
}
}
}
}

View File

@ -7,7 +7,7 @@
import SwiftUI
struct UsernameRow: View {
@State public var statusData: StatusData
@ObservedObject public var statusData: StatusData
var body: some View {
HStack (alignment: .center) {
@ -19,15 +19,15 @@ struct UsernameRow: View {
} placeholder: {
Image(systemName: "person.circle")
.resizable()
.foregroundColor(Color("mainTextColor"))
.foregroundColor(Color("MainTextColor"))
}
.frame(width: 48.0, height: 48.0)
VStack (alignment: .leading) {
Text(statusData.accountDisplayName ?? statusData.accountUsername)
.foregroundColor(Color("displayNameColor"))
.foregroundColor(Color("DisplayNameColor"))
Text("@\(statusData.accountUsername)")
.foregroundColor(Color("lightGrayColor"))
.foregroundColor(Color("LightGrayColor"))
.font(.footnote)
}
.padding(.leading, 8)