Few improvements

This commit is contained in:
Marcin Czachursk 2023-01-06 18:16:08 +01:00
parent 5a21331610
commit 4483d7500e
11 changed files with 236 additions and 40 deletions

6
README.md Normal file
View File

@ -0,0 +1,6 @@
# Vernissage
## Font
Font used in the application is: Fleur De Leah
https://fonts.google.com/specimen/Fleur+De+Leah?preview.text=Vernissage%20for&preview.text_type=custom

View File

@ -50,6 +50,8 @@
F866F6AA29605AFA002E8F88 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F6A929605AFA002E8F88 /* SceneDelegate.swift */; };
F866F6AE29606367002E8F88 /* ApplicationViewMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F6AD29606367002E8F88 /* ApplicationViewMode.swift */; };
F866F6B729608467002E8F88 /* MastodonSwift in Frameworks */ = {isa = PBXBuildFile; productRef = F866F6B629608467002E8F88 /* MastodonSwift */; };
F88ABD9229686F1C004EF61E /* MemoryCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88ABD9129686F1C004EF61E /* MemoryCache.swift */; };
F88ABD9429687CA4004EF61E /* ComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88ABD9329687CA4004EF61E /* ComposeView.swift */; };
F88C246C295C37B80006098B /* VernissageApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C246B295C37B80006098B /* VernissageApp.swift */; };
F88C246E295C37B80006098B /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C246D295C37B80006098B /* MainView.swift */; };
F88C2470295C37BB0006098B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F88C246F295C37BB0006098B /* Assets.xcassets */; };
@ -116,6 +118,9 @@
F866F6A829604FFF002E8F88 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
F866F6A929605AFA002E8F88 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
F866F6AD29606367002E8F88 /* ApplicationViewMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationViewMode.swift; sourceTree = "<group>"; };
F88ABD9129686F1C004EF61E /* MemoryCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryCache.swift; sourceTree = "<group>"; };
F88ABD9329687CA4004EF61E /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = "<group>"; };
F88ABD9529687D4D004EF61E /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
F88C2468295C37B80006098B /* Vernissage.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Vernissage.app; sourceTree = BUILT_PRODUCTS_DIR; };
F88C246B295C37B80006098B /* VernissageApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VernissageApp.swift; sourceTree = "<group>"; };
F88C246D295C37B80006098B /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = "<group>"; };
@ -179,6 +184,7 @@
F85DBF8E296732E20069BF89 /* FollowersView.swift */,
F85DBF902967385F0069BF89 /* FollowingView.swift */,
F897978E29684BCB00B22335 /* LoadingView.swift */,
F88ABD9329687CA4004EF61E /* ComposeView.swift */,
);
path = Views;
sourceTree = "<group>";
@ -256,9 +262,18 @@
path = Widgets;
sourceTree = "<group>";
};
F88ABD9029686F00004EF61E /* Cache */ = {
isa = PBXGroup;
children = (
F88ABD9129686F1C004EF61E /* MemoryCache.swift */,
);
path = Cache;
sourceTree = "<group>";
};
F88C245F295C37B80006098B = {
isa = PBXGroup;
children = (
F88ABD9529687D4D004EF61E /* README.md */,
F88C246A295C37B80006098B /* Vernissage */,
F88C2469295C37B80006098B /* Products */,
);
@ -276,6 +291,7 @@
isa = PBXGroup;
children = (
F866F6A829604FFF002E8F88 /* Info.plist */,
F88ABD9029686F00004EF61E /* Cache */,
F897978B2968367E00B22335 /* Haptics */,
F8210DE82966E4D8001D9973 /* Modifiers */,
F88FAD30295F5010009B20C9 /* Services */,
@ -446,9 +462,11 @@
F85D497F296416C800751DF7 /* CommentsSection.swift in Sources */,
F88C2486295C48030006098B /* HTMLFotmattedText.swift in Sources */,
F866F6A529604194002E8F88 /* ApplicationSettingsHandler.swift in Sources */,
F88ABD9229686F1C004EF61E /* MemoryCache.swift in Sources */,
F8210DE32966D256001D9973 /* Status+StatusData.swift in Sources */,
F85D49852964301800751DF7 /* StatusData+Attachments.swift in Sources */,
F8210DE72966E1D1001D9973 /* Color+Assets.swift in Sources */,
F88ABD9429687CA4004EF61E /* ComposeView.swift in Sources */,
F85D497D29640D5900751DF7 /* InteractionRow.swift in Sources */,
F866F6A729604629002E8F88 /* SignInView.swift in Sources */,
F88C246C295C37B80006098B /* VernissageApp.swift in Sources */,

View File

@ -0,0 +1,90 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the MIT License.
//
import Foundation
/// Memory cache based on article: https://www.swiftbysundell.com/articles/caching-in-swift/
final class MemoryCache<Key: Hashable, Value> {
private let wrapped = NSCache<WrappedKey, Entry>()
private let dateProvider: () -> Date
private let entryLifetime: TimeInterval
init(dateProvider: @escaping () -> Date = Date.init,
entryLifetime: TimeInterval = 12 * 60 * 60) {
self.dateProvider = dateProvider
self.entryLifetime = entryLifetime
}
func insert(_ value: Value, forKey key: Key) {
let date = dateProvider().addingTimeInterval(entryLifetime)
let entry = Entry(value: value, expirationDate: date)
wrapped.setObject(entry, forKey: WrappedKey(key))
}
func value(forKey key: Key) -> Value? {
guard let entry = wrapped.object(forKey: WrappedKey(key)) else {
return nil
}
guard dateProvider() < entry.expirationDate else {
// Discard values that have expired
removeValue(forKey: key)
return nil
}
return entry.value
}
func removeValue(forKey key: Key) {
wrapped.removeObject(forKey: WrappedKey(key))
}
}
private extension MemoryCache {
final class WrappedKey: NSObject {
let key: Key
init(_ key: Key) { self.key = key }
override var hash: Int { return key.hashValue }
override func isEqual(_ object: Any?) -> Bool {
guard let value = object as? WrappedKey else {
return false
}
return value.key == key
}
}
}
private extension MemoryCache {
final class Entry {
let value: Value
let expirationDate: Date
init(value: Value, expirationDate: Date) {
self.value = value
self.expirationDate = expirationDate
}
}
}
extension MemoryCache {
subscript(key: Key) -> Value? {
get { return value(forKey: key) }
set {
guard let value = newValue else {
// If nil was assigned using our subscript,
// then we remove any value for that key:
removeValue(forKey: key)
return
}
insert(value, forKey: key)
}
}
}

View File

@ -12,11 +12,11 @@ public class CacheAvatarService {
public static let shared = CacheAvatarService()
private init() { }
private var cache: Dictionary<String, UIImage> = [:]
private var memoryChartData = MemoryCache<String, UIImage>(entryLifetime: 5 * 60)
func addImage(for id: String, data: Data) {
if let uiImage = UIImage(data: data) {
self.cache[id] = uiImage
self.memoryChartData[id] = uiImage
}
}
@ -25,6 +25,10 @@ public class CacheAvatarService {
return
}
if memoryChartData[accountId] != nil {
return
}
do {
let avatarData = try await RemoteFileService.shared.fetchData(url: avatarUrl)
if let avatarData {
@ -36,6 +40,6 @@ public class CacheAvatarService {
}
func getImage(for id: String) -> UIImage? {
return self.cache[id]
return self.memoryChartData[id]
}
}

View File

@ -0,0 +1,46 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the MIT License.
//
import SwiftUI
struct ComposeView: View {
@Environment(\.dismiss) private var dismiss
var body: some View {
NavigationView {
VStack{
Text("Composen message placeholder")
.font(.caption2)
.foregroundColor(.mainTextColor)
}
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button {
dismiss()
} label: {
Text("Publish")
.foregroundColor(.white)
}
.buttonStyle(.borderedProminent)
.tint(.accentColor)
}
ToolbarItem(placement: .cancellationAction) {
Button("Cancel", role: .cancel) {
dismiss()
}
}
}
.navigationBarTitle(Text("Compose"), displayMode: .inline)
}
}
}
struct ComposeView_Previews: PreviewProvider {
static var previews: some View {
ComposeView()
}
}

View File

@ -31,18 +31,15 @@ struct FollowersView: View {
}
}
if allItemsLoaded == false && firstLoadFinished {
HStack(alignment: .center) {
Spacer()
LoadingIndicator()
.onAppear {
Task {
self.page = self.page + 1
await self.loadAccounts(page: self.page)
}
if allItemsLoaded == false && firstLoadFinished == true {
LoadingIndicator()
.onAppear {
Task {
self.page = self.page + 1
await self.loadAccounts(page: self.page)
}
Spacer()
}
}
.frame(idealWidth: .infinity, maxWidth: .infinity, alignment: .center)
}
}.overlay {
if firstLoadFinished == false {
@ -68,7 +65,7 @@ struct FollowersView: View {
andContext: self.applicationState.accountData,
page: page)
if accountsFromApi.isEmpty {
if accountsFromApi.isEmpty || accountsFromApi.count < 10 {
self.allItemsLoaded = true
return
}

View File

@ -31,18 +31,15 @@ struct FollowingView: View {
}
}
if allItemsLoaded == false && firstLoadFinished {
HStack(alignment: .center) {
Spacer()
LoadingIndicator()
.onAppear {
Task {
self.page = self.page + 1
await self.loadAccounts(page: self.page)
}
if allItemsLoaded == false && firstLoadFinished == true {
LoadingIndicator()
.onAppear {
Task {
self.page = self.page + 1
await self.loadAccounts(page: self.page)
}
Spacer()
}
}
.frame(idealWidth: .infinity, maxWidth: .infinity, alignment: .center)
}
}.overlay {
if firstLoadFinished == false {
@ -68,7 +65,7 @@ struct FollowingView: View {
andContext: self.applicationState.accountData,
page: page)
if accountsFromApi.isEmpty {
if accountsFromApi.isEmpty || accountsFromApi.count < 10 {
self.allItemsLoaded = true
return
}

View File

@ -12,6 +12,7 @@ struct StatusView: View {
@EnvironmentObject var applicationState: ApplicationState
@State var statusId: String
@State private var showCompose = false
@State private var statusData: StatusData?
var body: some View {
@ -54,12 +55,16 @@ struct StatusView: View {
.foregroundColor(.lightGrayColor)
.font(.footnote)
InteractionRow(statusData: statusData)
.padding(8)
InteractionRow(statusData: statusData) { context in
self.showCompose.toggle()
}
.padding(8)
}
.padding(8)
CommentsSection(statusId: statusData.id)
CommentsSection(statusId: statusData.id) { context in
self.showCompose.toggle()
}
}
} else {
VStack (alignment: .leading) {
@ -89,6 +94,9 @@ struct StatusView: View {
}
}
.navigationBarTitle("Details")
.sheet(isPresented: $showCompose, content: {
ComposeView()
})
.onAppear {
Task {
do {

View File

@ -14,6 +14,8 @@ struct CommentsSection: View {
@State public var withDivider = true
@State private var context: Context?
var onNewStatus: (_ context: Status) -> Void?
private let contentWidth = Int(UIScreen.main.bounds.width) - 50
var body: some View {
@ -52,20 +54,40 @@ struct CommentsSection: View {
VStack (alignment: .leading) {
HStack (alignment: .top) {
Text(status.account?.displayName ?? status.account?.acct ?? "")
Text(status.account?.displayName ?? status.account?.acct ?? status.account?.username ?? "")
.foregroundColor(.mainTextColor)
.font(.footnote)
.fontWeight(.bold)
Spacer()
Button {
HapticService.shared.touch()
onNewStatus(status)
} label: {
Image(systemName: "message")
.foregroundColor(.lightGrayColor)
.font(.footnote)
}
.padding(.trailing, 8)
Button {
HapticService.shared.touch()
// TODO: favorite
} label: {
Image(systemName: status.favourited ? "hand.thumbsup.fill" : "hand.thumbsup")
.foregroundColor(.lightGrayColor)
.font(.footnote)
}
.padding(.trailing, 8)
Text(status.createdAt.toRelative(.isoDateTimeMilliSec))
.foregroundColor(.lightGrayColor.opacity(0.5))
.foregroundColor(.lightGrayColor)
.font(.footnote)
}
HTMLFormattedText(status.content, withFontSize: 14, andWidth: contentWidth)
.padding(.top, -16)
.padding(.top, -10)
.padding(.leading, -4)
if status.mediaAttachments.count > 0 {
@ -97,7 +119,9 @@ struct CommentsSection: View {
.padding(.horizontal, 8)
.padding(.bottom, 8)
CommentsSection(statusId: status.id, withDivider: false)
CommentsSection(statusId: status.id, withDivider: false) { context in
onNewStatus(context)
}
}
}
}
@ -117,6 +141,6 @@ struct CommentsSection: View {
struct CommentsSection_Previews: PreviewProvider {
static var previews: some View {
CommentsSection(statusId: "", withDivider: true)
CommentsSection(statusId: "", withDivider: true) { context in }
}
}

View File

@ -10,11 +10,13 @@ struct InteractionRow: View {
@EnvironmentObject var applicationState: ApplicationState
@ObservedObject public var statusData: StatusData
var onNewStatus: (_ context: StatusData) -> Void?
var body: some View {
HStack (alignment: .top) {
Button {
// TODO: Reply.
HapticService.shared.touch()
onNewStatus(statusData)
} label: {
HStack(alignment: .center) {
Image(systemName: "message")
@ -120,7 +122,7 @@ struct InteractionRow: View {
struct InteractionRow_Previews: PreviewProvider {
static var previews: some View {
InteractionRow(statusData: PreviewData.getStatus())
InteractionRow(statusData: PreviewData.getStatus()) { context in }
.previewLayout(.fixed(width: 300, height: 70))
}
}

View File

@ -9,9 +9,13 @@ import SwiftUI
struct LoadingIndicator: View {
var body: some View {
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
.tint(.mainTextColor)
ProgressView {
Text("Loading...")
.foregroundColor(.mainTextColor)
.font(.caption2)
}
.progressViewStyle(CircularProgressViewStyle())
.tint(.mainTextColor)
}
}