Haptic feedback

This commit is contained in:
Marcin Czachursk 2023-01-06 13:05:21 +01:00
parent b3c542588c
commit 34a12eae75
18 changed files with 325 additions and 179 deletions

View File

@ -24,7 +24,6 @@
F8210DE52966E160001D9973 /* Color+SystemColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DE42966E160001D9973 /* Color+SystemColors.swift */; }; F8210DE52966E160001D9973 /* Color+SystemColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DE42966E160001D9973 /* Color+SystemColors.swift */; };
F8210DE72966E1D1001D9973 /* Color+Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DE62966E1D1001D9973 /* Color+Assets.swift */; }; F8210DE72966E1D1001D9973 /* Color+Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DE62966E1D1001D9973 /* Color+Assets.swift */; };
F8210DEA2966E4F9001D9973 /* AnimatePlaceholderModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DE92966E4F9001D9973 /* AnimatePlaceholderModifier.swift */; }; F8210DEA2966E4F9001D9973 /* AnimatePlaceholderModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DE92966E4F9001D9973 /* AnimatePlaceholderModifier.swift */; };
F8210DEC2966F30C001D9973 /* UserFeedbackService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DEB2966F30C001D9973 /* UserFeedbackService.swift */; };
F8341F90295C636C009C8EE6 /* UIImage+Exif.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8341F8F295C636C009C8EE6 /* UIImage+Exif.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 */; }; F8341F92295C63BB009C8EE6 /* ImageStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8341F91295C63BB009C8EE6 /* ImageStatus.swift */; };
F83901A6295D8EC000456AE2 /* LabelIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = F83901A5295D8EC000456AE2 /* LabelIcon.swift */; }; F83901A6295D8EC000456AE2 /* LabelIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = F83901A5295D8EC000456AE2 /* LabelIcon.swift */; };
@ -57,7 +56,7 @@
F88C2473295C37BB0006098B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F88C2472295C37BB0006098B /* Preview Assets.xcassets */; }; F88C2473295C37BB0006098B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F88C2472295C37BB0006098B /* Preview Assets.xcassets */; };
F88C2475295C37BB0006098B /* CoreDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C2474295C37BB0006098B /* CoreDataHandler.swift */; }; F88C2475295C37BB0006098B /* CoreDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C2474295C37BB0006098B /* CoreDataHandler.swift */; };
F88C2478295C37BB0006098B /* Vernissage.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = F88C2476295C37BB0006098B /* Vernissage.xcdatamodeld */; }; F88C2478295C37BB0006098B /* Vernissage.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = F88C2476295C37BB0006098B /* Vernissage.xcdatamodeld */; };
F88C2482295C3A4F0006098B /* DetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C2481295C3A4F0006098B /* DetailsView.swift */; }; F88C2482295C3A4F0006098B /* StatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C2481295C3A4F0006098B /* StatusView.swift */; };
F88C2486295C48030006098B /* HTMLFotmattedText.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C2485295C48030006098B /* HTMLFotmattedText.swift */; }; F88C2486295C48030006098B /* HTMLFotmattedText.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C2485295C48030006098B /* HTMLFotmattedText.swift */; };
F88FAD21295F3944009B20C9 /* HomeFeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD20295F3944009B20C9 /* HomeFeedView.swift */; }; F88FAD21295F3944009B20C9 /* HomeFeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD20295F3944009B20C9 /* HomeFeedView.swift */; };
F88FAD23295F3FC4009B20C9 /* LocalFeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD22295F3FC4009B20C9 /* LocalFeedView.swift */; }; F88FAD23295F3FC4009B20C9 /* LocalFeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD22295F3FC4009B20C9 /* LocalFeedView.swift */; };
@ -67,6 +66,9 @@
F88FAD2B295F43B8009B20C9 /* AccountData+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD29295F43B8009B20C9 /* AccountData+CoreDataProperties.swift */; }; F88FAD2B295F43B8009B20C9 /* AccountData+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD29295F43B8009B20C9 /* AccountData+CoreDataProperties.swift */; };
F88FAD2D295F4AD7009B20C9 /* ApplicationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD2C295F4AD7009B20C9 /* ApplicationState.swift */; }; F88FAD2D295F4AD7009B20C9 /* ApplicationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD2C295F4AD7009B20C9 /* ApplicationState.swift */; };
F88FAD32295F5029009B20C9 /* RemoteFileService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD31295F5029009B20C9 /* RemoteFileService.swift */; }; F88FAD32295F5029009B20C9 /* RemoteFileService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88FAD31295F5029009B20C9 /* RemoteFileService.swift */; };
F897978829681B9C00B22335 /* UserAvatar.swift in Sources */ = {isa = PBXBuildFile; fileRef = F897978729681B9C00B22335 /* UserAvatar.swift */; };
F897978A2968314A00B22335 /* LoadingIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F89797892968314A00B22335 /* LoadingIndicator.swift */; };
F897978D2968369600B22335 /* HapticService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F897978C2968369600B22335 /* HapticService.swift */; };
F8A93D7E2965FD89001D8331 /* UserProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A93D7D2965FD89001D8331 /* UserProfileView.swift */; }; F8A93D7E2965FD89001D8331 /* UserProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A93D7D2965FD89001D8331 /* UserProfileView.swift */; };
F8A93D802965FED4001D8331 /* AccountService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A93D7F2965FED4001D8331 /* AccountService.swift */; }; F8A93D802965FED4001D8331 /* AccountService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A93D7F2965FED4001D8331 /* AccountService.swift */; };
F8A93D822965FF5D001D8331 /* MastodonClientAuthenticated+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A93D812965FF5D001D8331 /* MastodonClientAuthenticated+Account.swift */; }; F8A93D822965FF5D001D8331 /* MastodonClientAuthenticated+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A93D812965FF5D001D8331 /* MastodonClientAuthenticated+Account.swift */; };
@ -87,7 +89,6 @@
F8210DE42966E160001D9973 /* Color+SystemColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+SystemColors.swift"; sourceTree = "<group>"; }; F8210DE42966E160001D9973 /* Color+SystemColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+SystemColors.swift"; sourceTree = "<group>"; };
F8210DE62966E1D1001D9973 /* Color+Assets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Assets.swift"; sourceTree = "<group>"; }; F8210DE62966E1D1001D9973 /* Color+Assets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Assets.swift"; sourceTree = "<group>"; };
F8210DE92966E4F9001D9973 /* AnimatePlaceholderModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatePlaceholderModifier.swift; sourceTree = "<group>"; }; F8210DE92966E4F9001D9973 /* AnimatePlaceholderModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatePlaceholderModifier.swift; sourceTree = "<group>"; };
F8210DEB2966F30C001D9973 /* UserFeedbackService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserFeedbackService.swift; sourceTree = "<group>"; };
F8341F8F295C636C009C8EE6 /* UIImage+Exif.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Exif.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>"; }; F8341F91295C63BB009C8EE6 /* ImageStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageStatus.swift; sourceTree = "<group>"; };
F83901A5295D8EC000456AE2 /* LabelIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelIcon.swift; sourceTree = "<group>"; }; F83901A5295D8EC000456AE2 /* LabelIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelIcon.swift; sourceTree = "<group>"; };
@ -121,7 +122,7 @@
F88C2472295C37BB0006098B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; }; F88C2472295C37BB0006098B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
F88C2474295C37BB0006098B /* CoreDataHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataHandler.swift; sourceTree = "<group>"; }; 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>"; }; F88C2477295C37BB0006098B /* Vernissage.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Vernissage.xcdatamodel; sourceTree = "<group>"; };
F88C2481295C3A4F0006098B /* DetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailsView.swift; 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>"; }; F88C2485295C48030006098B /* HTMLFotmattedText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTMLFotmattedText.swift; sourceTree = "<group>"; };
F88FAD20295F3944009B20C9 /* HomeFeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeFeedView.swift; sourceTree = "<group>"; }; F88FAD20295F3944009B20C9 /* HomeFeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeFeedView.swift; sourceTree = "<group>"; };
F88FAD22295F3FC4009B20C9 /* LocalFeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalFeedView.swift; sourceTree = "<group>"; }; F88FAD22295F3FC4009B20C9 /* LocalFeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalFeedView.swift; sourceTree = "<group>"; };
@ -131,6 +132,9 @@
F88FAD29295F43B8009B20C9 /* AccountData+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountData+CoreDataProperties.swift"; sourceTree = "<group>"; }; F88FAD29295F43B8009B20C9 /* AccountData+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountData+CoreDataProperties.swift"; sourceTree = "<group>"; };
F88FAD2C295F4AD7009B20C9 /* ApplicationState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationState.swift; sourceTree = "<group>"; }; F88FAD2C295F4AD7009B20C9 /* ApplicationState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationState.swift; sourceTree = "<group>"; };
F88FAD31295F5029009B20C9 /* RemoteFileService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteFileService.swift; sourceTree = "<group>"; }; F88FAD31295F5029009B20C9 /* RemoteFileService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteFileService.swift; sourceTree = "<group>"; };
F897978729681B9C00B22335 /* UserAvatar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAvatar.swift; sourceTree = "<group>"; };
F89797892968314A00B22335 /* LoadingIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingIndicator.swift; sourceTree = "<group>"; };
F897978C2968369600B22335 /* HapticService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticService.swift; sourceTree = "<group>"; };
F8A93D7D2965FD89001D8331 /* UserProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileView.swift; sourceTree = "<group>"; }; F8A93D7D2965FD89001D8331 /* UserProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileView.swift; sourceTree = "<group>"; };
F8A93D7F2965FED4001D8331 /* AccountService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountService.swift; sourceTree = "<group>"; }; F8A93D7F2965FED4001D8331 /* AccountService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountService.swift; sourceTree = "<group>"; };
F8A93D812965FF5D001D8331 /* MastodonClientAuthenticated+Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonClientAuthenticated+Account.swift"; sourceTree = "<group>"; }; F8A93D812965FF5D001D8331 /* MastodonClientAuthenticated+Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonClientAuthenticated+Account.swift"; sourceTree = "<group>"; };
@ -162,7 +166,7 @@
F8341F93295C63E2009C8EE6 /* Views */ = { F8341F93295C63E2009C8EE6 /* Views */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
F88C2481295C3A4F0006098B /* DetailsView.swift */, F88C2481295C3A4F0006098B /* StatusView.swift */,
F88C246D295C37B80006098B /* MainView.swift */, F88C246D295C37B80006098B /* MainView.swift */,
F88FAD20295F3944009B20C9 /* HomeFeedView.swift */, F88FAD20295F3944009B20C9 /* HomeFeedView.swift */,
F88FAD22295F3FC4009B20C9 /* LocalFeedView.swift */, F88FAD22295F3FC4009B20C9 /* LocalFeedView.swift */,
@ -243,6 +247,8 @@
F85D497A29640C8200751DF7 /* UsernameRow.swift */, F85D497A29640C8200751DF7 /* UsernameRow.swift */,
F85D497C29640D5900751DF7 /* InteractionRow.swift */, F85D497C29640D5900751DF7 /* InteractionRow.swift */,
F85D497E296416C800751DF7 /* CommentsSection.swift */, F85D497E296416C800751DF7 /* CommentsSection.swift */,
F897978729681B9C00B22335 /* UserAvatar.swift */,
F89797892968314A00B22335 /* LoadingIndicator.swift */,
); );
path = Widgets; path = Widgets;
sourceTree = "<group>"; sourceTree = "<group>";
@ -267,6 +273,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
F866F6A829604FFF002E8F88 /* Info.plist */, F866F6A829604FFF002E8F88 /* Info.plist */,
F897978B2968367E00B22335 /* Haptics */,
F8210DE82966E4D8001D9973 /* Modifiers */, F8210DE82966E4D8001D9973 /* Modifiers */,
F88FAD30295F5010009B20C9 /* Services */, F88FAD30295F5010009B20C9 /* Services */,
F83901A2295D863B00456AE2 /* Widgets */, F83901A2295D863B00456AE2 /* Widgets */,
@ -300,12 +307,19 @@
F85D4974296407F100751DF7 /* TimelineService.swift */, F85D4974296407F100751DF7 /* TimelineService.swift */,
F8A93D7F2965FED4001D8331 /* AccountService.swift */, F8A93D7F2965FED4001D8331 /* AccountService.swift */,
F8210DE02966D0C4001D9973 /* StatusService.swift */, F8210DE02966D0C4001D9973 /* StatusService.swift */,
F8210DEB2966F30C001D9973 /* UserFeedbackService.swift */,
F85DBF92296760790069BF89 /* CacheAvatarService.swift */, F85DBF92296760790069BF89 /* CacheAvatarService.swift */,
); );
path = Services; path = Services;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
F897978B2968367E00B22335 /* Haptics */ = {
isa = PBXGroup;
children = (
F897978C2968369600B22335 /* HapticService.swift */,
);
path = Haptics;
sourceTree = "<group>";
};
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXNativeTarget section */ /* Begin PBXNativeTarget section */
@ -391,6 +405,7 @@
F80048082961E6DE00E6868A /* StatusDataHandler.swift in Sources */, F80048082961E6DE00E6868A /* StatusDataHandler.swift in Sources */,
F866F6A0296040A8002E8F88 /* ApplicationSettings+CoreDataClass.swift in Sources */, F866F6A0296040A8002E8F88 /* ApplicationSettings+CoreDataClass.swift in Sources */,
F8210DEA2966E4F9001D9973 /* AnimatePlaceholderModifier.swift in Sources */, F8210DEA2966E4F9001D9973 /* AnimatePlaceholderModifier.swift in Sources */,
F897978A2968314A00B22335 /* LoadingIndicator.swift in Sources */,
F8210DE52966E160001D9973 /* Color+SystemColors.swift in Sources */, F8210DE52966E160001D9973 /* Color+SystemColors.swift in Sources */,
F85DBF93296760790069BF89 /* CacheAvatarService.swift in Sources */, F85DBF93296760790069BF89 /* CacheAvatarService.swift in Sources */,
F88FAD23295F3FC4009B20C9 /* LocalFeedView.swift in Sources */, F88FAD23295F3FC4009B20C9 /* LocalFeedView.swift in Sources */,
@ -405,6 +420,7 @@
F8A93D822965FF5D001D8331 /* MastodonClientAuthenticated+Account.swift in Sources */, F8A93D822965FF5D001D8331 /* MastodonClientAuthenticated+Account.swift in Sources */,
F85D49872964334100751DF7 /* String+Date.swift in Sources */, F85D49872964334100751DF7 /* String+Date.swift in Sources */,
F8341F92295C63BB009C8EE6 /* ImageStatus.swift in Sources */, F8341F92295C63BB009C8EE6 /* ImageStatus.swift in Sources */,
F897978829681B9C00B22335 /* UserAvatar.swift in Sources */,
F8210DDD2966CF17001D9973 /* StatusData+Status.swift in Sources */, F8210DDD2966CF17001D9973 /* StatusData+Status.swift in Sources */,
F8210DCF2966B600001D9973 /* ImageRowAsync.swift in Sources */, F8210DCF2966B600001D9973 /* ImageRowAsync.swift in Sources */,
F85D498329642FAC00751DF7 /* AttachmentData+Comperable.swift in Sources */, F85D498329642FAC00751DF7 /* AttachmentData+Comperable.swift in Sources */,
@ -416,12 +432,13 @@
F85DBF912967385F0069BF89 /* FollowingView.swift in Sources */, F85DBF912967385F0069BF89 /* FollowingView.swift in Sources */,
F800480A2961EA1900E6868A /* AttachmentDataHandler.swift in Sources */, F800480A2961EA1900E6868A /* AttachmentDataHandler.swift in Sources */,
F80048032961850500E6868A /* AttachmentData+CoreDataClass.swift in Sources */, F80048032961850500E6868A /* AttachmentData+CoreDataClass.swift in Sources */,
F897978D2968369600B22335 /* HapticService.swift in Sources */,
F8341F90295C636C009C8EE6 /* UIImage+Exif.swift in Sources */, F8341F90295C636C009C8EE6 /* UIImage+Exif.swift in Sources */,
F8A93D7E2965FD89001D8331 /* UserProfileView.swift in Sources */, F8A93D7E2965FD89001D8331 /* UserProfileView.swift in Sources */,
F85D4981296417F700751DF7 /* MastodonClientAuthenticated+Context.swift in Sources */, F85D4981296417F700751DF7 /* MastodonClientAuthenticated+Context.swift in Sources */,
F88C246E295C37B80006098B /* MainView.swift in Sources */, F88C246E295C37B80006098B /* MainView.swift in Sources */,
F88C2478295C37BB0006098B /* Vernissage.xcdatamodeld in Sources */, F88C2478295C37BB0006098B /* Vernissage.xcdatamodeld in Sources */,
F88C2482295C3A4F0006098B /* DetailsView.swift in Sources */, F88C2482295C3A4F0006098B /* StatusView.swift in Sources */,
F866F6A329604161002E8F88 /* AccountDataHandler.swift in Sources */, F866F6A329604161002E8F88 /* AccountDataHandler.swift in Sources */,
F85D497F296416C800751DF7 /* CommentsSection.swift in Sources */, F85D497F296416C800751DF7 /* CommentsSection.swift in Sources */,
F88C2486295C48030006098B /* HTMLFotmattedText.swift in Sources */, F88C2486295C48030006098B /* HTMLFotmattedText.swift in Sources */,
@ -442,7 +459,6 @@
F8A93D802965FED4001D8331 /* AccountService.swift in Sources */, F8A93D802965FED4001D8331 /* AccountService.swift in Sources */,
F866F6AA29605AFA002E8F88 /* SceneDelegate.swift in Sources */, F866F6AA29605AFA002E8F88 /* SceneDelegate.swift in Sources */,
F85D4973296406E700751DF7 /* BottomRight.swift in Sources */, F85D4973296406E700751DF7 /* BottomRight.swift in Sources */,
F8210DEC2966F30C001D9973 /* UserFeedbackService.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };

View File

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

View File

@ -0,0 +1,70 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the MIT License.
//
import CoreHaptics
public final class HapticService: ObservableObject {
public static let shared = HapticService()
private let hapticEngine: CHHapticEngine?
private var needsToRestart = false
/// Fires a transient haptic event with the given intensity and sharpness (0-1).
public func touch(intensity: Float = 0.75, sharpness: Float = 0.5) {
do {
let event = CHHapticEvent(
eventType: .hapticTransient,
parameters: [
CHHapticEventParameter(parameterID: .hapticIntensity, value: intensity),
CHHapticEventParameter(parameterID: .hapticSharpness, value: sharpness)
],
relativeTime: 0)
let pattern = try CHHapticPattern(events: [event], parameters: [])
let player = try hapticEngine?.makePlayer(with: pattern)
if needsToRestart {
try? start()
}
try player?.start(atTime: CHHapticTimeImmediate)
} catch {
print("Error \(error.localizedDescription)")
}
}
private init() {
hapticEngine = try? CHHapticEngine()
hapticEngine?.resetHandler = resetHandler
hapticEngine?.stoppedHandler = restartHandler
hapticEngine?.playsHapticsOnly = true
try? start()
}
/// Stops the internal CHHapticEngine. Should be called when your app enters the background.
public func stop(completionHandler: CHHapticEngine.CompletionHandler? = nil) {
hapticEngine?.stop(completionHandler: completionHandler)
}
/// Starts the internal CHHapticEngine. Should be called when your app enters the foreground.
public func start() throws {
try hapticEngine?.start()
needsToRestart = false
}
private func resetHandler() {
do {
try start()
} catch {
needsToRestart = true
}
}
private func restartHandler(_ reasonForStopping: CHHapticEngine.StoppedReason? = nil) {
resetHandler()
}
}

View File

@ -9,7 +9,8 @@ import Foundation
public class ApplicationState: ObservableObject { public class ApplicationState: ObservableObject {
public static let shared = ApplicationState() public static let shared = ApplicationState()
private init() { }
@Published var accountData: AccountData? @Published var accountData: AccountData?
} }

View File

@ -13,13 +13,28 @@ public class CacheAvatarService {
private init() { } private init() { }
private var cache: Dictionary<String, UIImage> = [:] private var cache: Dictionary<String, UIImage> = [:]
func addImage(for id: String, data: Data) { func addImage(for id: String, data: Data) {
if let uiImage = UIImage(data: data) { if let uiImage = UIImage(data: data) {
self.cache[id] = uiImage self.cache[id] = uiImage
} }
} }
func downloadImage(for accountId: String?, avatarUrl: URL?) async {
guard let accountId, let avatarUrl else {
return
}
do {
let avatarData = try await RemoteFileService.shared.fetchData(url: avatarUrl)
if let avatarData {
CacheAvatarService.shared.addImage(for: accountId, data: avatarData)
}
} catch {
print("Error \(error.localizedDescription)")
}
}
func getImage(for id: String) -> UIImage? { func getImage(for id: String) -> UIImage? {
return self.cache[id] return self.cache[id]
} }

View File

@ -1,18 +0,0 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the MIT License.
//
import SwiftUI
import AVFoundation
public class UserFeedbackService {
public static let shared = UserFeedbackService()
private init() { }
func send() {
AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
// AudioServicesPlaySystemSound(1016)
}
}

View File

@ -49,6 +49,12 @@ struct VernissageApp: App {
URLCache.shared.diskCapacity = 1_000_000_000 // ~1GB disk cache space URLCache.shared.diskCapacity = 1_000_000_000 // ~1GB disk cache space
} }
.navigationViewStyle(.stack) .navigationViewStyle(.stack)
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
try? HapticService.shared.start()
}
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didEnterBackgroundNotification)) { _ in
HapticService.shared.stop()
}
} }
} }
} }

View File

@ -14,25 +14,27 @@ struct FollowersView: View {
@State private var accounts: [Account] = [] @State private var accounts: [Account] = []
@State private var page = 1 @State private var page = 1
@State private var allItemsLoaded = false @State private var allItemsLoaded = false
@State private var firstLoadFinished = false
var body: some View { var body: some View {
List(accounts, id: \.id) { account in List {
NavigationLink(destination: UserProfileView( ForEach(accounts, id: \.id) { account in
accountId: account.id, NavigationLink(destination: UserProfileView(
accountDisplayName: account.displayName, accountId: account.id,
accountUserName: account.acct) accountDisplayName: account.displayName,
.environmentObject(applicationState)) { accountUserName: account.acct)
UsernameRow(accountAvatar: account.avatar, .environmentObject(applicationState)) {
accountDisplayName: account.displayName, UsernameRow(accountAvatar: account.avatar,
accountUsername: account.acct, accountDisplayName: account.displayName,
cachedAvatar: CacheAvatarService.shared.getImage(for: account.id)) accountUsername: account.acct,
} cachedAvatar: CacheAvatarService.shared.getImage(for: account.id))
}
if allItemsLoaded == false && accounts.last?.id == account.id { }
if allItemsLoaded == false && firstLoadFinished {
HStack(alignment: .center) { HStack(alignment: .center) {
Spacer() Spacer()
ProgressView() LoadingIndicator()
.progressViewStyle(CircularProgressViewStyle())
.onAppear { .onAppear {
Task { Task {
self.page = self.page + 1 self.page = self.page + 1
@ -42,6 +44,10 @@ struct FollowersView: View {
Spacer() Spacer()
} }
} }
}.overlay {
if firstLoadFinished == false {
LoadingIndicator()
}
} }
.navigationBarTitle("Followers") .navigationBarTitle("Followers")
.listStyle(PlainListStyle()) .listStyle(PlainListStyle())
@ -51,6 +57,7 @@ struct FollowersView: View {
} }
await self.loadAccounts(page: self.page) await self.loadAccounts(page: self.page)
self.firstLoadFinished = true
} }
} }
@ -66,25 +73,20 @@ struct FollowersView: View {
return return
} }
for account in accountsFromApi { await self.downloadAvatars(accounts: accountsFromApi)
guard let avatarUrl = account.avatar else {
continue
}
do {
if let avatarData = try await RemoteFileService.shared.fetchData(url: avatarUrl) {
CacheAvatarService.shared.addImage(for: account.id, data: avatarData)
}
} catch {
print("Error \(error.localizedDescription)")
}
}
self.accounts.append(contentsOf: accountsFromApi) self.accounts.append(contentsOf: accountsFromApi)
} catch { } catch {
print("Error \(error.localizedDescription)") print("Error \(error.localizedDescription)")
} }
} }
func downloadAvatars(accounts: [Account]) async {
await withTaskGroup(of: Void.self) { group in
for account in accounts {
group.addTask { await CacheAvatarService.shared.downloadImage(for: account.id, avatarUrl: account.avatar) }
}
}
}
} }
struct FollowersView_Previews: PreviewProvider { struct FollowersView_Previews: PreviewProvider {

View File

@ -14,25 +14,27 @@ struct FollowingView: View {
@State private var accounts: [Account] = [] @State private var accounts: [Account] = []
@State private var page = 1 @State private var page = 1
@State private var allItemsLoaded = false @State private var allItemsLoaded = false
@State private var firstLoadFinished = false
var body: some View { var body: some View {
List(accounts, id: \.id) { account in List {
NavigationLink(destination: UserProfileView( ForEach(accounts, id: \.id) { account in
accountId: account.id, NavigationLink(destination: UserProfileView(
accountDisplayName: account.displayName, accountId: account.id,
accountUserName: account.acct) accountDisplayName: account.displayName,
.environmentObject(applicationState)) { accountUserName: account.acct)
UsernameRow(accountAvatar: account.avatar, .environmentObject(applicationState)) {
accountDisplayName: account.displayName, UsernameRow(accountAvatar: account.avatar,
accountUsername: account.acct, accountDisplayName: account.displayName,
cachedAvatar: CacheAvatarService.shared.getImage(for: account.id)) accountUsername: account.acct,
} cachedAvatar: CacheAvatarService.shared.getImage(for: account.id))
}
if allItemsLoaded == false && accounts.last?.id == account.id { }
if allItemsLoaded == false && firstLoadFinished {
HStack(alignment: .center) { HStack(alignment: .center) {
Spacer() Spacer()
ProgressView() LoadingIndicator()
.progressViewStyle(CircularProgressViewStyle())
.onAppear { .onAppear {
Task { Task {
self.page = self.page + 1 self.page = self.page + 1
@ -42,6 +44,10 @@ struct FollowingView: View {
Spacer() Spacer()
} }
} }
}.overlay {
if firstLoadFinished == false {
LoadingIndicator()
}
} }
.navigationBarTitle("Following") .navigationBarTitle("Following")
.listStyle(PlainListStyle()) .listStyle(PlainListStyle())
@ -51,6 +57,7 @@ struct FollowingView: View {
} }
await self.loadAccounts(page: self.page) await self.loadAccounts(page: self.page)
self.firstLoadFinished = true
} }
} }
@ -66,29 +73,24 @@ struct FollowingView: View {
return return
} }
for account in accountsFromApi { await self.downloadAvatars(accounts: accountsFromApi)
guard let avatarUrl = account.avatar else {
continue
}
do {
if let avatarData = try await RemoteFileService.shared.fetchData(url: avatarUrl) {
CacheAvatarService.shared.addImage(for: account.id, data: avatarData)
}
} catch {
print("Error \(error.localizedDescription)")
}
}
self.accounts.append(contentsOf: accountsFromApi) self.accounts.append(contentsOf: accountsFromApi)
} catch { } catch {
print("Error \(error.localizedDescription)") print("Error \(error.localizedDescription)")
} }
} }
func downloadAvatars(accounts: [Account]) async {
await withTaskGroup(of: Void.self) { group in
for account in accounts {
group.addTask { await CacheAvatarService.shared.downloadImage(for: account.id, avatarUrl: account.avatar) }
}
}
}
} }
struct FollowingView_Previews: PreviewProvider { struct FollowingView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
FollowingView(accountId: "") FollowersView(accountId: "")
} }
} }

View File

@ -22,14 +22,13 @@ struct HomeFeedView: View {
ScrollView { ScrollView {
LazyVGrid(columns: gridColumns) { LazyVGrid(columns: gridColumns) {
ForEach(dbStatuses, id: \.self) { item in ForEach(dbStatuses, id: \.self) { item in
NavigationLink(destination: DetailsView(statusId: item.id) NavigationLink(destination: StatusView(statusId: item.id)
.environmentObject(applicationState)) { .environmentObject(applicationState)) {
ImageRow(attachments: item.attachments()) ImageRow(attachments: item.attachments())
} }
} }
ProgressView() LoadingIndicator()
.progressViewStyle(CircularProgressViewStyle())
.onAppear { .onAppear {
Task { Task {
do { do {
@ -45,8 +44,7 @@ struct HomeFeedView: View {
} }
if showLoading { if showLoading {
ProgressView() LoadingIndicator()
.progressViewStyle(CircularProgressViewStyle())
} }
} }
.refreshable { .refreshable {

View File

@ -47,7 +47,7 @@ struct MainView: View {
if let accountData = self.applicationState.accountData { if let accountData = self.applicationState.accountData {
UserProfileView(accountId: accountData.id, UserProfileView(accountId: accountData.id,
accountDisplayName: accountData.displayName, accountDisplayName: accountData.displayName,
accountUserName: accountData.username) accountUserName: accountData.acct)
} }
case .notifications: case .notifications:
NotificationsView() NotificationsView()
@ -112,7 +112,7 @@ struct MainView: View {
.font(.subheadline) .font(.subheadline)
} }
.frame(width: 150) .frame(width: 150)
.foregroundColor(Color.mainTextColor) .foregroundColor(.mainTextColor)
} }
} }
} }
@ -126,10 +126,10 @@ struct MainView: View {
// TODO: Switch accounts. // TODO: Switch accounts.
} label: { } label: {
HStack { HStack {
Text(self.applicationState.accountData?.displayName ?? self.applicationState.accountData?.username ?? "") Text(self.applicationState.accountData?.displayName ?? self.applicationState.accountData?.acct ?? "")
Image(systemName: "person.circle.fill") Image(systemName: "person.circle.fill")
.resizable() .resizable()
.foregroundColor(Color.mainTextColor) .foregroundColor(.mainTextColor)
} }
} }
@ -153,7 +153,7 @@ struct MainView: View {
Image(systemName: "person.circle") Image(systemName: "person.circle")
.resizable() .resizable()
.frame(width: 32.0, height: 32.0) .frame(width: 32.0, height: 32.0)
.foregroundColor(Color.mainTextColor) .foregroundColor(.mainTextColor)
} }
} }
} }

View File

@ -8,7 +8,7 @@ import SwiftUI
import MastodonSwift import MastodonSwift
import AVFoundation import AVFoundation
struct DetailsView: View { struct StatusView: View {
@EnvironmentObject var applicationState: ApplicationState @EnvironmentObject var applicationState: ApplicationState
@State var statusId: String @State var statusId: String
@ -41,7 +41,7 @@ struct DetailsView: View {
LabelIcon(iconName: "calendar", value: "2 Oct 2022") LabelIcon(iconName: "calendar", value: "2 Oct 2022")
} }
.padding(.bottom, 2) .padding(.bottom, 2)
.foregroundColor(Color.lightGrayColor) .foregroundColor(.lightGrayColor)
HStack { HStack {
Text("Uploaded") Text("Uploaded")
@ -51,7 +51,7 @@ struct DetailsView: View {
Text("via \(applicationName)") Text("via \(applicationName)")
} }
} }
.foregroundColor(Color.lightGrayColor) .foregroundColor(.lightGrayColor)
.font(.footnote) .font(.footnote)
InteractionRow(statusData: statusData) InteractionRow(statusData: statusData)
@ -72,10 +72,10 @@ struct DetailsView: View {
accountUsername: "@username") accountUsername: "@username")
Text("Lorem ispum text something") Text("Lorem ispum text something")
.foregroundColor(Color.lightGrayColor) .foregroundColor(.lightGrayColor)
.font(.footnote) .font(.footnote)
Text("Lorem ispum text something sdf sdfsdf sdfdsfsdfsdf") Text("Lorem ispum text something sdf sdfsdf sdfdsfsdfsdf")
.foregroundColor(Color.lightGrayColor) .foregroundColor(.lightGrayColor)
.font(.footnote) .font(.footnote)
LabelIcon(iconName: "camera", value: "SONY ILCE-7M3") LabelIcon(iconName: "camera", value: "SONY ILCE-7M3")
@ -114,8 +114,8 @@ struct DetailsView: View {
} }
} }
struct DetailsView_Previews: PreviewProvider { struct StatusView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
DetailsView(statusId: "123") StatusView(statusId: "123")
} }
} }

View File

@ -8,13 +8,15 @@ import SwiftUI
import MastodonSwift import MastodonSwift
struct UserProfileView: View { struct UserProfileView: View {
@EnvironmentObject var applicationState: ApplicationState @EnvironmentObject private var applicationState: ApplicationState
@State public var accountId: String @State public var accountId: String
@State public var accountDisplayName: String? @State public var accountDisplayName: String?
@State public var accountUserName: String @State public var accountUserName: String
@State private var account: Account? = nil @State private var account: Account? = nil
@State private var relationship: Relationship? = nil @State private var relationship: Relationship? = nil
@State private var statuses: [Status] = [] @State private var statuses: [Status] = []
@State private var isDuringRelationshipAction = false
private static let initialColumns = 1 private static let initialColumns = 1
@State private var gridColumns = Array(repeating: GridItem(.flexible()), count: initialColumns) @State private var gridColumns = Array(repeating: GridItem(.flexible()), count: initialColumns)
@ -24,17 +26,7 @@ struct UserProfileView: View {
if let account = self.account { if let account = self.account {
VStack(alignment: .leading) { VStack(alignment: .leading) {
HStack(alignment: .center) { HStack(alignment: .center) {
AsyncImage(url: account.avatar) { image in UserAvatar(accountAvatar: account.avatar, width: 96, height: 96)
image
.resizable()
.clipShape(Circle())
.aspectRatio(contentMode: .fit)
} placeholder: {
Image(systemName: "person.circle")
.resizable()
.foregroundColor(Color.mainTextColor)
}
.frame(width: 96.0, height: 96.0)
Spacer() Spacer()
@ -58,7 +50,7 @@ struct UserProfileView: View {
.font(.subheadline) .font(.subheadline)
.opacity(0.6) .opacity(0.6)
} }
}.foregroundColor(Color.mainTextColor) }.foregroundColor(.mainTextColor)
Spacer() Spacer()
@ -72,17 +64,17 @@ struct UserProfileView: View {
.font(.subheadline) .font(.subheadline)
.opacity(0.6) .opacity(0.6)
} }
}.foregroundColor(Color.mainTextColor) }.foregroundColor(.mainTextColor)
} }
HStack (alignment: .center) { HStack (alignment: .center) {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text(account.displayName ?? account.username) Text(account.displayName ?? account.acct)
.foregroundColor(Color.mainTextColor) .foregroundColor(.mainTextColor)
.font(.footnote) .font(.footnote)
.fontWeight(.bold) .fontWeight(.bold)
Text("@\(account.username)") Text("@\(account.acct)")
.foregroundColor(Color.lightGrayColor) .foregroundColor(.lightGrayColor)
.font(.footnote) .font(.footnote)
} }
@ -91,12 +83,17 @@ struct UserProfileView: View {
if self.applicationState.accountData?.id != self.accountId { if self.applicationState.accountData?.id != self.accountId {
Button { Button {
Task { Task {
Task { @MainActor in
self.isDuringRelationshipAction = false
}
HapticService.shared.touch()
self.isDuringRelationshipAction = true
do { do {
if let relationship = try await AccountService.shared.follow( if let relationship = try await AccountService.shared.follow(
forAccountId: self.accountId, forAccountId: self.accountId,
andContext: self.applicationState.accountData andContext: self.applicationState.accountData
) { ) {
UserFeedbackService.shared.send()
self.relationship = relationship self.relationship = relationship
} }
} catch { } catch {
@ -104,13 +101,18 @@ struct UserProfileView: View {
} }
} }
} label: { } label: {
HStack { if isDuringRelationshipAction {
Image(systemName: relationship?.following == true ? "person.badge.minus" : "person.badge.plus") LoadingIndicator()
Text(relationship?.following == true ? "Unfollow" : (relationship?.followedBy == true ? "Follow back" : "Follow")) } else {
HStack {
Image(systemName: relationship?.following == true ? "person.badge.minus" : "person.badge.plus")
Text(relationship?.following == true ? "Unfollow" : (relationship?.followedBy == true ? "Follow back" : "Follow"))
}
} }
} }
.disabled(isDuringRelationshipAction)
.buttonStyle(.borderedProminent) .buttonStyle(.borderedProminent)
.tint(relationship?.following == true ? Color.dangerColor : .accentColor) .tint(relationship?.following == true ? .dangerColor : .accentColor)
} }
} }
@ -121,7 +123,7 @@ struct UserProfileView: View {
} }
Text("Joined \(account.createdAt.toRelative(.isoDateTimeMilliSec))") Text("Joined \(account.createdAt.toRelative(.isoDateTimeMilliSec))")
.foregroundColor(Color.lightGrayColor.opacity(0.5)) .foregroundColor(.lightGrayColor.opacity(0.5))
.font(.footnote) .font(.footnote)
} }
@ -129,7 +131,7 @@ struct UserProfileView: View {
LazyVGrid(columns: gridColumns) { LazyVGrid(columns: gridColumns) {
ForEach(self.statuses, id: \.id) { item in ForEach(self.statuses, id: \.id) { item in
NavigationLink(destination: DetailsView(statusId: item.id) NavigationLink(destination: StatusView(statusId: item.id)
.environmentObject(applicationState)) { .environmentObject(applicationState)) {
ImageRowAsync(attachments: item.mediaAttachments) ImageRowAsync(attachments: item.mediaAttachments)
} }
@ -137,8 +139,7 @@ struct UserProfileView: View {
} }
} else { } else {
ProgressView() LoadingIndicator()
.progressViewStyle(CircularProgressViewStyle())
} }
} }
.navigationBarTitle(self.accountDisplayName ?? self.accountUserName) .navigationBarTitle(self.accountDisplayName ?? self.accountUserName)
@ -147,7 +148,8 @@ struct UserProfileView: View {
do { do {
async let relationshipTask = AccountService.shared.getRelationship(withId: self.accountId, forUser: self.applicationState.accountData) async let relationshipTask = AccountService.shared.getRelationship(withId: self.accountId, forUser: self.applicationState.accountData)
async let accountTask = AccountService.shared.getAccount(withId: self.accountId, and: self.applicationState.accountData) async let accountTask = AccountService.shared.getAccount(withId: self.accountId, and: self.applicationState.accountData)
// Wait for download account and relationships.
(self.relationship, self.account) = try await (relationshipTask, accountTask) (self.relationship, self.account) = try await (relationshipTask, accountTask)
self.statuses = try await AccountService.shared.getStatuses(forAccountId: self.accountId, andContext: self.applicationState.accountData) self.statuses = try await AccountService.shared.getStatuses(forAccountId: self.accountId, andContext: self.applicationState.accountData)

View File

@ -34,7 +34,7 @@ struct CommentsSection: View {
NavigationLink(destination: UserProfileView( NavigationLink(destination: UserProfileView(
accountId: account.id, accountId: account.id,
accountDisplayName: account.displayName, accountDisplayName: account.displayName,
accountUserName: account.username) accountUserName: account.acct)
.environmentObject(applicationState)) { .environmentObject(applicationState)) {
AsyncImage(url: account.avatar) { image in AsyncImage(url: account.avatar) { image in
image image
@ -44,7 +44,7 @@ struct CommentsSection: View {
} placeholder: { } placeholder: {
Image(systemName: "person.circle") Image(systemName: "person.circle")
.resizable() .resizable()
.foregroundColor(Color.mainTextColor) .foregroundColor(.mainTextColor)
} }
.frame(width: 32.0, height: 32.0) .frame(width: 32.0, height: 32.0)
} }
@ -52,23 +52,20 @@ struct CommentsSection: View {
VStack (alignment: .leading) { VStack (alignment: .leading) {
HStack (alignment: .top) { HStack (alignment: .top) {
Text(status.account?.displayName ?? status.account?.username ?? "") Text(status.account?.displayName ?? status.account?.acct ?? "")
.foregroundColor(Color.mainTextColor) .foregroundColor(.mainTextColor)
.font(.footnote) .font(.footnote)
.fontWeight(.bold) .fontWeight(.bold)
Text("@\(status.account?.username ?? "")")
.foregroundColor(Color.lightGrayColor)
.font(.footnote)
Spacer() Spacer()
Text(status.createdAt.toRelative(.isoDateTimeMilliSec)) Text(status.createdAt.toRelative(.isoDateTimeMilliSec))
.foregroundColor(Color.lightGrayColor.opacity(0.5)) .foregroundColor(.lightGrayColor.opacity(0.5))
.font(.footnote) .font(.footnote)
} }
HTMLFormattedText(status.content, withFontSize: 14, andWidth: contentWidth) HTMLFormattedText(status.content, withFontSize: 14, andWidth: contentWidth)
.padding(.top, -10) .padding(.top, -16)
.padding(.leading, -4) .padding(.leading, -4)
if status.mediaAttachments.count > 0 { if status.mediaAttachments.count > 0 {
@ -81,14 +78,14 @@ struct CommentsSection: View {
.frame(minWidth: 0, maxWidth: .infinity) .frame(minWidth: 0, maxWidth: .infinity)
.frame(height: status.mediaAttachments.count == 1 ? 200 : 100) .frame(height: status.mediaAttachments.count == 1 ? 200 : 100)
.cornerRadius(10) .cornerRadius(10)
.shadow(color: Color.mainTextColor.opacity(0.3), radius: 2) .shadow(color: .mainTextColor.opacity(0.3), radius: 2)
} placeholder: { } placeholder: {
Image(systemName: "photo") Image(systemName: "photo")
.resizable() .resizable()
.scaledToFit() .scaledToFit()
.frame(minWidth: 0, maxWidth: .infinity) .frame(minWidth: 0, maxWidth: .infinity)
.frame(height: status.mediaAttachments.count == 1 ? 200 : 100) .frame(height: status.mediaAttachments.count == 1 ? 200 : 100)
.foregroundColor(Color.mainTextColor) .foregroundColor(.mainTextColor)
.opacity(0.05) .opacity(0.05)
} }
} }

View File

@ -9,12 +9,12 @@ import SwiftUI
struct InteractionRow: View { struct InteractionRow: View {
@EnvironmentObject var applicationState: ApplicationState @EnvironmentObject var applicationState: ApplicationState
@ObservedObject public var statusData: StatusData @ObservedObject public var statusData: StatusData
var body: some View { var body: some View {
HStack (alignment: .top) { HStack (alignment: .top) {
Button { Button {
// TODO: Reply. // TODO: Reply.
UserFeedbackService.shared.send() HapticService.shared.touch()
} label: { } label: {
HStack(alignment: .center) { HStack(alignment: .center) {
Image(systemName: "message") Image(systemName: "message")
@ -27,6 +27,8 @@ struct InteractionRow: View {
Button { Button {
Task { Task {
HapticService.shared.touch()
do { do {
let status = self.statusData.reblogged let status = self.statusData.reblogged
? try await StatusService.shared.unboost(statusId: self.statusData.id, accountData: self.applicationState.accountData) ? try await StatusService.shared.unboost(statusId: self.statusData.id, accountData: self.applicationState.accountData)
@ -38,7 +40,6 @@ struct InteractionRow: View {
: Int32(status.reblogsCount) : Int32(status.reblogsCount)
self.statusData.reblogged = status.reblogged self.statusData.reblogged = status.reblogged
UserFeedbackService.shared.send()
} }
} catch { } catch {
print("Error \(error.localizedDescription)") print("Error \(error.localizedDescription)")
@ -56,6 +57,8 @@ struct InteractionRow: View {
Button { Button {
Task { Task {
HapticService.shared.touch()
do { do {
let status = self.statusData.favourited let status = self.statusData.favourited
? try await StatusService.shared.unfavourite(statusId: self.statusData.id, accountData: self.applicationState.accountData) ? try await StatusService.shared.unfavourite(statusId: self.statusData.id, accountData: self.applicationState.accountData)
@ -67,7 +70,6 @@ struct InteractionRow: View {
: Int32(status.favouritesCount) : Int32(status.favouritesCount)
self.statusData.favourited = status.favourited self.statusData.favourited = status.favourited
UserFeedbackService.shared.send()
} }
} catch { } catch {
print("Error \(error.localizedDescription)") print("Error \(error.localizedDescription)")
@ -85,13 +87,14 @@ struct InteractionRow: View {
Button { Button {
Task { Task {
HapticService.shared.touch()
do { do {
let status = self.statusData.bookmarked _ = self.statusData.bookmarked
? try await StatusService.shared.unbookmark(statusId: self.statusData.id, accountData: self.applicationState.accountData) ? try await StatusService.shared.unbookmark(statusId: self.statusData.id, accountData: self.applicationState.accountData)
: try await StatusService.shared.bookmark(statusId: self.statusData.id, accountData: self.applicationState.accountData) : try await StatusService.shared.bookmark(statusId: self.statusData.id, accountData: self.applicationState.accountData)
self.statusData.bookmarked.toggle() self.statusData.bookmarked.toggle()
UserFeedbackService.shared.send()
} catch { } catch {
print("Error \(error.localizedDescription)") print("Error \(error.localizedDescription)")
} }
@ -104,14 +107,14 @@ struct InteractionRow: View {
Button { Button {
// TODO: Share. // TODO: Share.
UserFeedbackService.shared.send() HapticService.shared.touch()
} label: { } label: {
Image(systemName: "square.and.arrow.up") Image(systemName: "square.and.arrow.up")
} }
} }
.font(.title3) .font(.title3)
.fontWeight(.semibold) .fontWeight(.semibold)
.foregroundColor(Color.accentColor) .foregroundColor(.accentColor)
} }
} }

View File

@ -0,0 +1,22 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the MIT License.
//
import SwiftUI
struct LoadingIndicator: View {
var body: some View {
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
.tint(.mainTextColor)
}
}
struct LoadingIndicator_Previews: PreviewProvider {
static var previews: some View {
LoadingIndicator()
}
}

View File

@ -0,0 +1,47 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the MIT License.
//
import SwiftUI
struct UserAvatar: View {
@State public var accountAvatar: URL?
@State public var cachedAvatar: UIImage?
@State public var width = 48.0
@State public var height = 48.0
var body: some View {
if let cachedAvatar {
Image(uiImage: cachedAvatar)
.resizable()
.clipShape(Circle())
.aspectRatio(contentMode: .fit)
.frame(width: self.width, height: self.height)
}
else {
AsyncImage(url: self.accountAvatar) { image in
image
.resizable()
.clipShape(Circle())
.aspectRatio(contentMode: .fit)
} placeholder: {
Image(systemName: "person.circle")
.resizable()
.clipShape(Circle())
.aspectRatio(contentMode: .fit)
.foregroundColor(.mainTextColor)
}
.frame(width: self.width, height: self.height)
}
}
}
struct UserAvatar_Previews: PreviewProvider {
static var previews: some View {
UserAvatar()
.previewLayout(.fixed(width: 128, height: 128))
}
}

View File

@ -15,34 +15,16 @@ struct UsernameRow: View {
var body: some View { var body: some View {
HStack (alignment: .center) { HStack (alignment: .center) {
if let cachedAvatar { UserAvatar(accountAvatar: accountAvatar,
Image(uiImage: cachedAvatar) cachedAvatar: cachedAvatar,
.resizable() width: 48,
.clipShape(Circle()) height: 48)
.aspectRatio(contentMode: .fit)
.frame(width: 48.0, height: 48.0)
}
else {
AsyncImage(url: accountAvatar) { image in
image
.resizable()
.clipShape(Circle())
.aspectRatio(contentMode: .fit)
} placeholder: {
Image(systemName: "person.circle")
.resizable()
.clipShape(Circle())
.aspectRatio(contentMode: .fit)
.foregroundColor(Color.mainTextColor)
}
.frame(width: 48.0, height: 48.0)
}
VStack (alignment: .leading) { VStack (alignment: .leading) {
Text(accountDisplayName ?? accountUsername) Text(accountDisplayName ?? accountUsername)
.foregroundColor(Color.mainTextColor) .foregroundColor(.mainTextColor)
Text("@\(accountUsername)") Text("@\(accountUsername)")
.foregroundColor(Color.lightGrayColor) .foregroundColor(.lightGrayColor)
.font(.footnote) .font(.footnote)
} }
.padding(.leading, 8) .padding(.leading, 8)
@ -52,6 +34,7 @@ struct UsernameRow: View {
struct UsernameRow_Previews: PreviewProvider { struct UsernameRow_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
UsernameRow(accountUsername: "") UsernameRow(accountDisplayName: "John Doe", accountUsername: "johndoe@mastodon.xx")
.previewLayout(.fixed(width: 320, height: 64))
} }
} }