Add first implementation of reblogs on timeline

This commit is contained in:
Marcin Czachurski 2023-09-29 16:03:42 +02:00
parent 15bf208496
commit d8aa3db3ee
25 changed files with 259 additions and 58 deletions

View File

@ -12,8 +12,9 @@ extension Client {
public func getHomeTimeline(maxId: String? = nil,
sinceId: String? = nil,
minId: String? = nil,
limit: Int = 40) async throws -> [Status] {
return try await pixelfedClient.getHomeTimeline(maxId: maxId, sinceId: sinceId, minId: minId, limit: limit)
limit: Int = 40,
includeReblogs: Bool? = nil) async throws -> [Status] {
return try await pixelfedClient.getHomeTimeline(maxId: maxId, sinceId: sinceId, minId: minId, limit: limit, includeReblogs: includeReblogs)
}
public func getStatuses(local: Bool? = nil,

View File

@ -45,7 +45,7 @@ public class StatusModel: ObservableObject {
// If status has been rebloged we are saving orginal status here.
let orginalStatus = status.reblog ?? status
self.id = orginalStatus.id
self.id = status.id
self.content = orginalStatus.content
self.uri = orginalStatus.uri
self.url = orginalStatus.url

View File

@ -34,6 +34,7 @@ extension ApplicationSettings {
@NSManaged public var showAltIconOnTimeline: Bool
@NSManaged public var warnAboutMissingAlt: Bool
@NSManaged public var showGridOnUserProfile: Bool
@NSManaged public var showReboostedStatuses: Bool
@NSManaged public var customNavigationMenuItem1: Int32
@NSManaged public var customNavigationMenuItem2: Int32

View File

@ -59,6 +59,7 @@ class ApplicationSettingsHandler {
applicationState.showAltIconOnTimeline = defaultSettings.showAltIconOnTimeline
applicationState.warnAboutMissingAlt = defaultSettings.warnAboutMissingAlt
applicationState.showGridOnUserProfile = defaultSettings.showGridOnUserProfile
applicationState.showReboostedStatuses = defaultSettings.showReboostedStatuses
if let menuPosition = MenuPosition(rawValue: Int(defaultSettings.menuPosition)) {
applicationState.menuPosition = menuPosition
@ -197,6 +198,12 @@ class ApplicationSettingsHandler {
CoreDataHandler.shared.save()
}
func set(showReboostedStatuses: Bool) {
let defaultSettings = self.get()
defaultSettings.showReboostedStatuses = showReboostedStatuses
CoreDataHandler.shared.save()
}
private func createApplicationSettingsEntity(viewContext: NSManagedObjectContext? = nil) -> ApplicationSettings {
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
return ApplicationSettings(context: context)

View File

@ -12,7 +12,8 @@ extension StatusData {
if let reblog = status.reblog {
self.copyFrom(reblog)
self.rebloggedStatusId = status.id
self.id = status.id
self.rebloggedStatusId = reblog.id
self.rebloggedAccountAvatar = status.account.avatar
self.rebloggedAccountDisplayName = status.account.displayName
self.rebloggedAccountId = status.account.id

View File

@ -3,6 +3,6 @@
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>Vernissage-014.xcdatamodel</string>
<string>Vernissage-015.xcdatamodel</string>
</dict>
</plist>

View File

@ -0,0 +1,104 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22222" systemVersion="23A344" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="AccountData" representedClassName="AccountData" syncable="YES">
<attribute name="accessToken" optional="YES" attributeType="String"/>
<attribute name="acct" attributeType="String"/>
<attribute name="avatar" optional="YES" attributeType="URI"/>
<attribute name="avatarData" optional="YES" attributeType="Binary"/>
<attribute name="clientId" attributeType="String"/>
<attribute name="clientSecret" attributeType="String"/>
<attribute name="clientVapidKey" attributeType="String"/>
<attribute name="createdAt" attributeType="String"/>
<attribute name="displayName" optional="YES" attributeType="String"/>
<attribute name="followersCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="followingCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="header" optional="YES" attributeType="URI"/>
<attribute name="id" attributeType="String"/>
<attribute name="lastSeenStatusId" optional="YES" attributeType="String"/>
<attribute name="locked" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="note" optional="YES" attributeType="String"/>
<attribute name="refreshToken" optional="YES" attributeType="String"/>
<attribute name="serverUrl" attributeType="URI"/>
<attribute name="statusesCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="url" optional="YES" attributeType="URI"/>
<attribute name="username" attributeType="String"/>
<relationship name="statuses" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="StatusData" inverseName="pixelfedAccount" inverseEntity="StatusData"/>
</entity>
<entity name="ApplicationSettings" representedClassName="ApplicationSettings" syncable="YES">
<attribute name="activeIcon" attributeType="String" defaultValueString="Default"/>
<attribute name="avatarShape" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="YES"/>
<attribute name="currentAccount" optional="YES" attributeType="String"/>
<attribute name="customNavigationMenuItem1" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="YES"/>
<attribute name="customNavigationMenuItem2" attributeType="Integer 32" defaultValueString="2" usesScalarValueType="YES"/>
<attribute name="customNavigationMenuItem3" attributeType="Integer 32" defaultValueString="5" usesScalarValueType="YES"/>
<attribute name="hapticAnimationEnabled" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="hapticButtonPressEnabled" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="hapticNotificationEnabled" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="hapticRefreshEnabled" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="hapticTabSelectionEnabled" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="lastRefreshTokens" attributeType="Date" defaultDateTimeInterval="694256400" usesScalarValueType="NO"/>
<attribute name="menuPosition" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="YES"/>
<attribute name="showAltIconOnTimeline" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="showAvatarsOnTimeline" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="showFavouritesOnTimeline" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="showGridOnUserProfile" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="showPhotoDescription" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="showReboostedStatuses" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="showSensitive" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="theme" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="tintColor" attributeType="Integer 32" defaultValueString="2" usesScalarValueType="YES"/>
<attribute name="warnAboutMissingAlt" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
</entity>
<entity name="AttachmentData" representedClassName="AttachmentData" syncable="YES">
<attribute name="blurhash" optional="YES" attributeType="String"/>
<attribute name="data" optional="YES" attributeType="Binary" allowsExternalBinaryDataStorage="YES"/>
<attribute name="exifCamera" optional="YES" attributeType="String"/>
<attribute name="exifCreatedDate" optional="YES" attributeType="String"/>
<attribute name="exifExposure" optional="YES" attributeType="String"/>
<attribute name="exifLens" optional="YES" attributeType="String"/>
<attribute name="id" attributeType="String"/>
<attribute name="metaImageHeight" optional="YES" attributeType="Integer 32" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="metaImageWidth" optional="YES" attributeType="Integer 32" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="order" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="previewUrl" optional="YES" attributeType="URI"/>
<attribute name="remoteUrl" optional="YES" attributeType="URI"/>
<attribute name="statusId" attributeType="String"/>
<attribute name="text" optional="YES" attributeType="String"/>
<attribute name="type" attributeType="String"/>
<attribute name="url" attributeType="URI"/>
<relationship name="statusRelation" maxCount="1" deletionRule="Nullify" destinationEntity="StatusData" inverseName="attachmentsRelation" inverseEntity="StatusData"/>
</entity>
<entity name="StatusData" representedClassName="StatusData" syncable="YES">
<attribute name="accountAvatar" optional="YES" attributeType="URI"/>
<attribute name="accountDisplayName" optional="YES" attributeType="String"/>
<attribute name="accountId" attributeType="String"/>
<attribute name="accountUsername" optional="YES" attributeType="String"/>
<attribute name="applicationName" optional="YES" attributeType="String"/>
<attribute name="applicationWebsite" optional="YES" attributeType="URI"/>
<attribute name="bookmarked" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="content" attributeType="String"/>
<attribute name="createdAt" attributeType="String"/>
<attribute name="favourited" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="favouritesCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="id" attributeType="String"/>
<attribute name="inReplyToAccount" optional="YES" attributeType="String"/>
<attribute name="inReplyToId" optional="YES" attributeType="String"/>
<attribute name="muted" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="pinned" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="reblogged" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="rebloggedAccountAvatar" optional="YES" attributeType="URI"/>
<attribute name="rebloggedAccountDisplayName" optional="YES" attributeType="String"/>
<attribute name="rebloggedAccountId" optional="YES" attributeType="String"/>
<attribute name="rebloggedAccountUsername" optional="YES" attributeType="String"/>
<attribute name="rebloggedStatusId" optional="YES" attributeType="String"/>
<attribute name="reblogsCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="repliesCount" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="sensitive" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="spoilerText" optional="YES" attributeType="String"/>
<attribute name="uri" attributeType="String"/>
<attribute name="url" optional="YES" attributeType="URI"/>
<attribute name="visibility" attributeType="String"/>
<relationship name="attachmentsRelation" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="AttachmentData" inverseName="statusRelation" inverseEntity="AttachmentData"/>
<relationship name="pixelfedAccount" maxCount="1" deletionRule="Nullify" destinationEntity="AccountData" inverseName="statuses" inverseEntity="AccountData"/>
</entity>
</model>

View File

@ -107,6 +107,9 @@ public class ApplicationState: ObservableObject {
/// Show grid of photos on user profile.
@Published public var showGridOnUserProfile = false
/// Show reboosted statuses on home timeline.
@Published public var showReboostedStatuses = false
public func changeApplicationState(accountModel: AccountModel, instance: Instance?, lastSeenStatusId: String?) {
self.account = accountModel
self.lastSeenStatusId = lastSeenStatusId

View File

@ -240,6 +240,8 @@
"settings.title.showAltTextOnTimeline" = "ALT icon will be displayed on timelines";
"settings.title.warnAboutMissingAltTitle" = "Warn of missing ALT text";
"settings.title.warnAboutMissingAltDescription" = "A warning about missing ALT texts will be displayed before publishing new post.";
"settings.title.enableReboostOnTimeline" = "Show boosted statuses";
"settings.title.enableReboostOnTimelineDescription" = "Boosted statuses will be visible on your home timeline.";
// Mark: Signin view.
"signin.navigationBar.title" = "Sign in to Pixelfed";

View File

@ -240,6 +240,8 @@
"settings.title.showAltTextOnTimeline" = "ALT ikurra (deskribapena edo testu alternatiboa dagoenaren seinale) denbora-lerroan erakutsiko da";
"settings.title.warnAboutMissingAltTitle" = "Abisatu ALT ahaztu bazait";
"settings.title.warnAboutMissingAltDescription" = "Irudiren batek deskribapenik ez badu, argitaratu baino lehen abisua erakutsiko da.";
"settings.title.enableReboostOnTimeline" = "Show boosted statuses";
"settings.title.enableReboostOnTimelineDescription" = "Boosted statuses will be visible on your home timeline.";
// Mark: Signin view.
"signin.navigationBar.title" = "Hasi saioa Pixelfed-en";

View File

@ -240,6 +240,8 @@
"settings.title.showAltTextOnTimeline" = "L'icône ALT sera affichée sur la timeline";
"settings.title.warnAboutMissingAltTitle" = "Avertir de l'absence de texte ALT";
"settings.title.warnAboutMissingAltDescription" = "Un avertissement concernant les textes ALT manquants sera affiché avant la publication d'un nouveau message.";
"settings.title.enableReboostOnTimeline" = "Show boosted statuses";
"settings.title.enableReboostOnTimelineDescription" = "Boosted statuses will be visible on your home timeline.";
// Mark: Signin view.
"signin.navigationBar.title" = "Se connecter à Pixelfed";

View File

@ -240,6 +240,8 @@
"settings.title.showAltTextOnTimeline" = "Ikony ALT będą widonczne na osiach zdjęć";
"settings.title.warnAboutMissingAltTitle" = "Ostrzeganie o brakującym tekście ALT";
"settings.title.warnAboutMissingAltDescription" = "Ostrzeżenie o brakujących tekstach ALT będzie wyświetlane przed opublikowaniem nowego statusu.";
"settings.title.enableReboostOnTimeline" = "Wyświetl podbite statusy";
"settings.title.enableReboostOnTimelineDescription" = "Podbite statusy będą widoczne na twojej osi czasu.";
// Mark: Signin view.
"signin.navigationBar.title" = "Zaloguj się do Pixelfed";

View File

@ -11,11 +11,12 @@ public extension PixelfedClientAuthenticated {
maxId: EntityId? = nil,
sinceId: EntityId? = nil,
minId: EntityId? = nil,
limit: Int? = nil) async throws -> [Status] {
limit: Int? = nil,
includeReblogs: Bool? = nil) async throws -> [Status] {
let request = try Self.request(
for: baseURL,
target: Pixelfed.Timelines.home(maxId, sinceId, minId, limit),
target: Pixelfed.Timelines.home(maxId, sinceId, minId, limit, includeReblogs),
withBearerToken: token
)

View File

@ -8,7 +8,7 @@ import Foundation
extension Pixelfed {
public enum Timelines {
case home(MaxId?, SinceId?, MinId?, Limit?)
case home(MaxId?, SinceId?, MinId?, Limit?, Bool?)
case pub(Bool?, Bool?, Bool?, MaxId?, SinceId?, MinId?, Limit?)
case tag(String, Bool?, Bool?, Bool?, MaxId?, SinceId?, MinId?, Limit?)
}
@ -43,6 +43,7 @@ extension Pixelfed.Timelines: TargetType {
var local: Bool?
var remote: Bool?
var onlyMedia: Bool?
var includeReblogs: Bool?
var maxId: MaxId?
var sinceId: SinceId?
var minId: MinId?
@ -58,34 +59,46 @@ extension Pixelfed.Timelines: TargetType {
sinceId = paramSinceId
minId = paramMinId
limit = paramLimit
case .home(let paramMaxId, let paramSinceId, let paramMinId, let paramLimit):
case .home(let paramMaxId, let paramSinceId, let paramMinId, let paramLimit, let paramIncludeReblogs):
maxId = paramMaxId
sinceId = paramSinceId
minId = paramMinId
limit = paramLimit
includeReblogs = paramIncludeReblogs
}
if let maxId {
params.append(("max_id", maxId))
}
if let sinceId {
params.append(("since_id", sinceId))
}
if let minId {
params.append(("min_id", minId))
}
if let limit {
params.append(("limit", "\(limit)"))
}
if let local {
params.append(("local", local.asString))
}
if let remote {
params.append(("remote", remote.asString))
}
if let onlyMedia {
params.append(("only_media", onlyMedia.asString))
}
if let includeReblogs, includeReblogs == true {
params.append(("include_reblogs", includeReblogs.asString))
}
return params
}

View File

@ -332,6 +332,7 @@
F878842129A4A4E3003CFAD2 /* AppMetadataService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppMetadataService.swift; sourceTree = "<group>"; };
F87AEB912986C44E00434FB6 /* AuthorizationSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizationSession.swift; sourceTree = "<group>"; };
F87AEB962986D16D00434FB6 /* AuthorisationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorisationError.swift; sourceTree = "<group>"; };
F880EECE2AC70A2B00C09C31 /* Vernissage-015.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-015.xcdatamodel"; sourceTree = "<group>"; };
F883401F29B62AE900C3E096 /* SearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchView.swift; sourceTree = "<group>"; };
F88AB05229B3613900345EDE /* PhotoUrl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoUrl.swift; sourceTree = "<group>"; };
F88AB05429B3626300345EDE /* ImageGrid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGrid.swift; sourceTree = "<group>"; };
@ -1805,6 +1806,7 @@
F88C2476295C37BB0006098B /* Vernissage.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
F880EECE2AC70A2B00C09C31 /* Vernissage-015.xcdatamodel */,
F8206A032A06547600E19412 /* Vernissage-014.xcdatamodel */,
F865B4D42A0252FB008ACDFC /* Vernissage-013.xcdatamodel */,
F8EF3C8B29FC3A5F00CBFF7C /* Vernissage-012.xcdatamodel */,
@ -1821,7 +1823,7 @@
F8C937A929882CA90004D782 /* Vernissage-001.xcdatamodel */,
F88C2477295C37BB0006098B /* Vernissage.xcdatamodel */,
);
currentVersion = F8206A032A06547600E19412 /* Vernissage-014.xcdatamodel */;
currentVersion = F880EECE2AC70A2B00C09C31 /* Vernissage-015.xcdatamodel */;
path = Vernissage.xcdatamodeld;
sourceTree = "<group>";
versionGroupType = wrapper.xcdatamodel;

View File

@ -20,7 +20,7 @@ public class HomeTimelineService {
private let imagePrefetcher = ImagePrefetcher(destination: .diskCache)
@MainActor
public func loadOnBottom(for account: AccountModel) async throws -> Int {
public func loadOnBottom(for account: AccountModel, includeReblogs: Bool) async throws -> Int {
// Load data from API and operate on CoreData on background context.
let backgroundContext = CoreDataHandler.shared.newBackgroundContext()
@ -32,7 +32,7 @@ public class HomeTimelineService {
}
// Load data on bottom of the list.
let allStatusesFromApi = try await self.load(for: account, on: backgroundContext, maxId: oldestStatus.id)
let allStatusesFromApi = try await self.load(for: account, includeReblogs: includeReblogs, on: backgroundContext, maxId: oldestStatus.id)
// Save data into database.
CoreDataHandler.shared.save(viewContext: backgroundContext)
@ -45,7 +45,7 @@ public class HomeTimelineService {
}
@MainActor
public func refreshTimeline(for account: AccountModel, updateLastSeenStatus: Bool = false) async throws -> String? {
public func refreshTimeline(for account: AccountModel, includeReblogs: Bool, updateLastSeenStatus: Bool = false) async throws -> String? {
// Load data from API and operate on CoreData on background context.
let backgroundContext = CoreDataHandler.shared.newBackgroundContext()
@ -55,7 +55,7 @@ public class HomeTimelineService {
// Refresh/load home timeline (refreshing on top downloads always first 40 items).
// When Apple introduce good way to show new items without scroll to top then we can change that method.
let allStatusesFromApi = try await self.refresh(for: account, on: backgroundContext)
let allStatusesFromApi = try await self.refresh(for: account, includeReblogs: includeReblogs, on: backgroundContext)
// Update last seen status.
if let lastSeenStatusId, updateLastSeenStatus == true {
@ -107,7 +107,7 @@ public class HomeTimelineService {
CoreDataHandler.shared.save()
}
public func amountOfNewStatuses(for account: AccountModel) async -> Int {
public func amountOfNewStatuses(for account: AccountModel, includeReblogs: Bool) async -> Int {
guard let accessToken = account.accessToken else {
return 0
}
@ -128,7 +128,7 @@ public class HomeTimelineService {
// There can be more then 40 newest statuses, that's why we have to sometimes send more then one request.
while true {
do {
let downloadedStatuses = try await client.getHomeTimeline(minId: newestStatusId, limit: self.defaultAmountOfDownloadedStatuses)
let downloadedStatuses = try await client.getHomeTimeline(minId: newestStatusId, limit: self.defaultAmountOfDownloadedStatuses, includeReblogs: includeReblogs)
guard let firstStatus = downloadedStatuses.first else {
break
}
@ -147,14 +147,14 @@ public class HomeTimelineService {
return amountOfStatuses
}
private func refresh(for account: AccountModel, on backgroundContext: NSManagedObjectContext) async throws -> [Status] {
private func refresh(for account: AccountModel, includeReblogs: Bool, on backgroundContext: NSManagedObjectContext) async throws -> [Status] {
guard let accessToken = account.accessToken else {
return []
}
// Retrieve statuses from API.
let client = PixelfedClient(baseURL: account.serverUrl).getAuthenticated(token: accessToken)
let statuses = try await client.getHomeTimeline(limit: self.defaultAmountOfDownloadedStatuses)
let statuses = try await client.getHomeTimeline(limit: self.defaultAmountOfDownloadedStatuses, includeReblogs: includeReblogs)
// Update all existing statuses in database.
for status in statuses {
@ -207,6 +207,7 @@ public class HomeTimelineService {
}
private func load(for account: AccountModel,
includeReblogs: Bool,
on backgroundContext: NSManagedObjectContext,
minId: String? = nil,
maxId: String? = nil
@ -217,7 +218,7 @@ public class HomeTimelineService {
// Retrieve statuses from API.
let client = PixelfedClient(baseURL: account.serverUrl).getAuthenticated(token: accessToken)
let statuses = try await client.getHomeTimeline(maxId: maxId, minId: minId, limit: self.defaultAmountOfDownloadedStatuses)
let statuses = try await client.getHomeTimeline(maxId: maxId, minId: minId, limit: self.defaultAmountOfDownloadedStatuses, includeReblogs: includeReblogs)
// Save statuses in database.
try await self.add(statuses, for: account, on: backgroundContext)

View File

@ -204,7 +204,8 @@ struct VernissageApp: App {
private func calculateNewPhotosInBackground() async {
if let account = self.applicationState.account {
self.applicationState.amountOfNewStatuses = await HomeTimelineService.shared.amountOfNewStatuses(for: account)
self.applicationState.amountOfNewStatuses = await HomeTimelineService.shared.amountOfNewStatuses(for: account,
includeReblogs: self.applicationState.showReboostedStatuses)
}
}
}

View File

@ -69,7 +69,7 @@ struct HomeFeedView: View {
.task {
do {
if let account = self.applicationState.account {
let newStatusesCount = try await HomeTimelineService.shared.loadOnBottom(for: account)
let newStatusesCount = try await HomeTimelineService.shared.loadOnBottom(for: account, includeReblogs: self.applicationState.showReboostedStatuses)
if newStatusesCount == 0 {
allItemsLoaded = true
}
@ -101,7 +101,7 @@ struct HomeFeedView: View {
private func refreshData() async {
do {
if let account = self.applicationState.account {
let lastSeenStatusId = try await HomeTimelineService.shared.refreshTimeline(for: account, updateLastSeenStatus: true)
let lastSeenStatusId = try await HomeTimelineService.shared.refreshTimeline(for: account, includeReblogs: self.applicationState.showReboostedStatuses, updateLastSeenStatus: true)
asyncAfter(0.35) {
self.applicationState.lastSeenStatusId = lastSeenStatusId
@ -125,7 +125,7 @@ struct HomeFeedView: View {
}
if let account = self.applicationState.account {
_ = try await HomeTimelineService.shared.refreshTimeline(for: account)
_ = try await HomeTimelineService.shared.refreshTimeline(for: account, includeReblogs: self.applicationState.showReboostedStatuses)
}
self.applicationState.amountOfNewStatuses = 0

View File

@ -85,6 +85,18 @@ struct MediaSettingsView: View {
.onChange(of: self.applicationState.warnAboutMissingAlt) { newValue in
ApplicationSettingsHandler.shared.set(warnAboutMissingAlt: newValue)
}
Toggle(isOn: $applicationState.showReboostedStatuses) {
VStack(alignment: .leading) {
Text("settings.title.enableReboostOnTimeline", comment: "Show boosted statuses")
Text("settings.title.enableReboostOnTimelineDescription", comment: "Boosted statuses will be visible on your home timeline.")
.font(.footnote)
.foregroundColor(.customGrayColor)
}
}
.onChange(of: self.applicationState.showReboostedStatuses) { newValue in
ApplicationSettingsHandler.shared.set(showReboostedStatuses: newValue)
}
}
}
}

View File

@ -239,7 +239,8 @@ struct StatusesView: View {
maxId: maxId,
sinceId: sinceId,
minId: minId,
limit: self.defaultLimit) ?? []
limit: self.defaultLimit,
includeReblogs: self.applicationState.showReboostedStatuses) ?? []
case .local:
return try await self.client.publicTimeline?.getStatuses(
local: true,

View File

@ -61,7 +61,10 @@ struct ImageRowItem: View {
} blurred: {
ZStack {
BlurredImage(blurhash: attachmentData.blurhash)
ImageAvatar(displayName: self.status.accountDisplayName, avatarUrl: self.status.accountAvatar) {
ImageAvatar(displayName: self.status.accountDisplayName,
avatarUrl: self.status.accountAvatar,
rebloggedAccountDisplayName: self.status.rebloggedAccountDisplayName,
rebloggedAccountAvatar: self.status.rebloggedAccountAvatar) {
self.routerPath.navigate(to: .userProfile(accountId: self.status.accountId,
accountDisplayName: self.status.accountDisplayName,
accountUserName: self.status.accountUsername))
@ -141,7 +144,10 @@ struct ImageRowItem: View {
ZStack {
self.imageView(uiImage: uiImage)
ImageAvatar(displayName: self.status.accountDisplayName, avatarUrl: self.status.accountAvatar) {
ImageAvatar(displayName: self.status.accountDisplayName,
avatarUrl: self.status.accountAvatar,
rebloggedAccountDisplayName: self.status.rebloggedAccountDisplayName,
rebloggedAccountAvatar: self.status.rebloggedAccountAvatar) {
self.routerPath.navigate(to: .userProfile(accountId: self.status.accountId,
accountDisplayName: self.status.accountDisplayName,
accountUserName: self.status.accountUsername))
@ -155,6 +161,23 @@ struct ImageRowItem: View {
FavouriteTouch(showFavouriteAnimation: $showThumbImage)
}
}
@ViewBuilder
func reblogInformation() -> some View {
if let rebloggedAccountAvatar = self.status.rebloggedAccountAvatar,
let rebloggedAccountDisplayName = self.status.rebloggedAccountDisplayName {
HStack(alignment: .center, spacing: 4) {
UserAvatar(accountAvatar: rebloggedAccountAvatar, size: .mini)
Text(rebloggedAccountDisplayName)
Image("custom.rocket")
.padding(.trailing, 8)
}
.font(.footnote)
.foregroundColor(Color.mainTextColor.opacity(0.4))
.background(Color.mainTextColor.opacity(0.1))
.clipShape(Capsule())
}
}
@ViewBuilder
private func imageView(uiImage: UIImage) -> some View {
@ -228,7 +251,7 @@ struct ImageRowItem: View {
private func navigateToStatus() {
self.routerPath.navigate(to: .status(
id: status.rebloggedStatusId ?? status.id,
id: status.id,
blurhash: status.attachments().first?.blurhash,
highestImageUrl: status.attachments().getHighestImage()?.url,
metaImageWidth: status.attachments().first?.metaImageWidth,

View File

@ -67,7 +67,9 @@ struct ImageRowItemAsync: View {
BlurredImage(blurhash: attachment.blurhash)
if self.showAvatar {
ImageAvatar(displayName: self.statusViewModel.account.displayNameWithoutEmojis,
avatarUrl: self.statusViewModel.account.avatar) {
avatarUrl: self.statusViewModel.account.avatar,
rebloggedAccountDisplayName: self.statusViewModel.reblogStatus?.account.displayNameWithoutEmojis,
rebloggedAccountAvatar: self.statusViewModel.reblogStatus?.account.avatar) {
self.routerPath.navigate(to: .userProfile(accountId: self.statusViewModel.account.id,
accountDisplayName: self.statusViewModel.account.displayNameWithoutEmojis,
accountUserName: self.statusViewModel.account.acct))
@ -143,7 +145,9 @@ struct ImageRowItemAsync: View {
if self.showAvatar {
ImageAvatar(displayName: self.statusViewModel.account.displayNameWithoutEmojis,
avatarUrl: self.statusViewModel.account.avatar) {
avatarUrl: self.statusViewModel.account.avatar,
rebloggedAccountDisplayName: self.statusViewModel.reblogStatus?.account.displayNameWithoutEmojis,
rebloggedAccountAvatar: self.statusViewModel.reblogStatus?.account.avatar) {
self.routerPath.navigate(to: .userProfile(accountId: self.statusViewModel.account.id,
accountDisplayName: self.statusViewModel.account.displayNameWithoutEmojis,
accountUserName: self.statusViewModel.account.acct))

View File

@ -27,7 +27,7 @@ public class StatusFetcher {
}
let client = PixelfedClient(baseURL: account.serverUrl).getAuthenticated(token: accessToken)
let statuses = try await client.getHomeTimeline(limit: 20)
let statuses = try await client.getHomeTimeline(limit: 20, includeReblogs: defaultSettings.showReboostedStatuses)
var widgetEntries: [PhotoWidgetEntry] = []
for status in statuses {

View File

@ -29,7 +29,8 @@ public struct ContentWarning<Content: View, Blurred: View>: View {
.transition(.opacity)
VStack(alignment: .trailing) {
HStack(alignment: .top) {
Spacer()
HStack(alignment: .bottom) {
Spacer()
Button {
withAnimation {
@ -40,12 +41,10 @@ public struct ContentWarning<Content: View, Blurred: View>: View {
.font(.system(size: 18))
.foregroundColor(.white.opacity(0.8))
.shadow(color: Color.black, radius: 1)
.padding(.top, 11)
.padding(.trailing, 16)
.padding([.bottom, .leading], 16)
.padding([.top, .bottom, .leading], 12)
}
}
Spacer()
.padding(.trailing, 64)
}
.foregroundColor(.mainTextColor)
}

View File

@ -14,43 +14,62 @@ public struct ImageAvatar: View {
private let displayName: String?
private let avatarUrl: URL?
private let rebloggedAccountDisplayName: String?
private let rebloggedAccountAvatar: URL?
private let onTap: () -> Void
public init(displayName: String?, avatarUrl: URL?, onTap: @escaping () -> Void) {
public init(displayName: String?, avatarUrl: URL?, rebloggedAccountDisplayName: String?, rebloggedAccountAvatar: URL?, onTap: @escaping () -> Void) {
self.displayName = displayName
self.avatarUrl = avatarUrl
self.rebloggedAccountAvatar = rebloggedAccountAvatar
self.rebloggedAccountDisplayName = rebloggedAccountDisplayName
self.onTap = onTap
}
public var body: some View {
if self.applicationState.showAvatarsOnTimeline {
VStack(alignment: .leading) {
VStack(alignment: .leading, spacing: 2) {
HStack(alignment: .center) {
HStack(alignment: .center) {
LazyImage(url: avatarUrl) { state in
if let image = state.image {
self.buildAvatar(image: image)
} else if state.isLoading {
self.buildAvatar()
} else {
self.buildAvatar()
}
LazyImage(url: avatarUrl) { state in
if let image = state.image {
self.buildAvatar(image: image)
} else if state.isLoading {
self.buildAvatar()
} else {
self.buildAvatar()
}
Text(displayName ?? "")
.font(.system(size: 15))
.foregroundColor(.white.opacity(0.8))
.fontWeight(.semibold)
.shadow(color: .black, radius: 2)
}
.padding(8)
.onTapGesture {
self.onTap()
}
Text(displayName ?? "")
.lineLimit(1)
.font(.system(size: 15))
.foregroundColor(.white.opacity(0.8))
.fontWeight(.semibold)
.shadow(color: .black, radius: 2)
Spacer()
if let rebloggedAccountAvatar = self.rebloggedAccountAvatar,
let rebloggedAccountDisplayName = self.rebloggedAccountDisplayName {
HStack(alignment: .center, spacing: 4) {
UserAvatar(accountAvatar: rebloggedAccountAvatar, size: .mini)
Text(rebloggedAccountDisplayName)
.lineLimit(1)
Image("custom.rocket")
.padding(.trailing, 8)
}
.font(.footnote)
.foregroundColor(.white.opacity(0.8))
.background(.black.opacity(0.4))
.clipShape(Capsule())
.padding(.leading, 32)
}
}
.padding(8)
.onTapGesture {
self.onTap()
}
Spacer()
}
}