Merge pull request #82 from VernissageApp/feature/reblogs
Feature/reblogs
|
@ -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,
|
||||
|
|
|
@ -9,6 +9,7 @@ import PixelfedKit
|
|||
|
||||
public class StatusModel: ObservableObject {
|
||||
public let id: EntityId
|
||||
public let rebloggedStatusId: EntityId?
|
||||
public let content: Html
|
||||
|
||||
public let uri: String?
|
||||
|
@ -41,11 +42,12 @@ public class StatusModel: ObservableObject {
|
|||
@Published public var mediaAttachments: [AttachmentModel]
|
||||
|
||||
public init(status: Status) {
|
||||
self.id = status.id
|
||||
self.rebloggedStatusId = status.reblog?.id
|
||||
|
||||
// If status has been rebloged we are saving orginal status here.
|
||||
let orginalStatus = status.reblog ?? status
|
||||
|
||||
self.id = orginalStatus.id
|
||||
self.content = orginalStatus.content
|
||||
self.uri = orginalStatus.uri
|
||||
self.url = orginalStatus.url
|
||||
|
@ -86,6 +88,13 @@ public class StatusModel: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
public extension StatusModel {
|
||||
/// Function returns status Id for real status (status with images), even for reboosted statuses.
|
||||
func getOrginalStatusId() -> EntityId {
|
||||
return self.rebloggedStatusId ?? self.id
|
||||
}
|
||||
}
|
||||
|
||||
public extension StatusModel {
|
||||
func getImageWidth() -> Int32? {
|
||||
let highestImage = self.mediaAttachments.getHighestImage()
|
||||
|
|
|
@ -34,6 +34,7 @@ extension AccountData {
|
|||
@NSManaged public var url: URL?
|
||||
@NSManaged public var username: String
|
||||
@NSManaged public var statuses: Set<StatusData>?
|
||||
@NSManaged public var viewedStatuses: Set<ViewedStatus>?
|
||||
@NSManaged public var lastSeenStatusId: String?
|
||||
}
|
||||
|
||||
|
@ -51,6 +52,18 @@ extension AccountData {
|
|||
|
||||
@objc(removeStatuses:)
|
||||
@NSManaged public func removeFromStatuses(_ values: NSSet)
|
||||
|
||||
@objc(addViewedStatusesObject:)
|
||||
@NSManaged public func addToViewedStatuses(_ value: ViewedStatus)
|
||||
|
||||
@objc(removeViewedStatusesObject:)
|
||||
@NSManaged public func removeFromViewedStatuses(_ value: ViewedStatus)
|
||||
|
||||
@objc(addViewedStatuses:)
|
||||
@NSManaged public func addToViewedStatuses(_ values: NSSet)
|
||||
|
||||
@objc(removeViewedStatuses:)
|
||||
@NSManaged public func removeFromViewedStatuses(_ values: NSSet)
|
||||
}
|
||||
|
||||
extension AccountData: Identifiable {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -5,12 +5,14 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import OSLog
|
||||
import EnvironmentKit
|
||||
|
||||
public class CoreDataError {
|
||||
public static let shared = CoreDataError()
|
||||
private init() { }
|
||||
|
||||
public func handle(_ error: Error, message: String) {
|
||||
print("Error ['\(message)']: \(error.localizedDescription)")
|
||||
Logger.main.error("Error ['\(message)']: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
//
|
||||
|
||||
import CoreData
|
||||
import OSLog
|
||||
import EnvironmentKit
|
||||
|
||||
public class CoreDataHandler {
|
||||
|
@ -50,7 +51,7 @@ public class CoreDataHandler {
|
|||
do {
|
||||
try coordinator.migratePersistentStore(oldStore, to: storeURL, options: nil, withType: NSSQLiteStoreType)
|
||||
} catch {
|
||||
print(error.localizedDescription)
|
||||
Logger.main.error("\(error.localizedDescription)")
|
||||
}
|
||||
|
||||
// Delete old store.
|
||||
|
@ -59,7 +60,7 @@ public class CoreDataHandler {
|
|||
do {
|
||||
try FileManager.default.removeItem(at: url)
|
||||
} catch {
|
||||
print(error.localizedDescription)
|
||||
Logger.main.error("\(error.localizedDescription)")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -12,7 +12,9 @@ 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
|
||||
|
@ -78,3 +80,9 @@ extension StatusData {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension StatusData {
|
||||
func getOrginalStatusId() -> String {
|
||||
return self.rebloggedStatusId ?? self.id
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,6 @@
|
|||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>_XCCurrentVersionName</key>
|
||||
<string>Vernissage-014.xcdatamodel</string>
|
||||
<string>Vernissage-017.xcdatamodel</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
@ -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>
|
|
@ -0,0 +1,110 @@
|
|||
<?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"/>
|
||||
<relationship name="viewedStatuses" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="ViewedStatus" inverseName="pixelfedAccount" inverseEntity="ViewedStatus"/>
|
||||
</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>
|
||||
<entity name="ViewedStatus" representedClassName="ViewedStatus" syncable="YES">
|
||||
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="id" attributeType="String"/>
|
||||
<relationship name="pixelfedAccount" maxCount="1" deletionRule="Nullify" destinationEntity="AccountData" inverseName="viewedStatuses" inverseEntity="AccountData"/>
|
||||
</entity>
|
||||
</model>
|
|
@ -0,0 +1,111 @@
|
|||
<?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"/>
|
||||
<relationship name="viewedStatuses" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="ViewedStatus" inverseName="pixelfedAccount" inverseEntity="ViewedStatus"/>
|
||||
</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>
|
||||
<entity name="ViewedStatus" representedClassName="ViewedStatus" syncable="YES">
|
||||
<attribute name="date" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="id" attributeType="String"/>
|
||||
<attribute name="reblogId" optional="YES" attributeType="String"/>
|
||||
<relationship name="pixelfedAccount" maxCount="1" deletionRule="Nullify" destinationEntity="AccountData" inverseName="viewedStatuses" inverseEntity="AccountData"/>
|
||||
</entity>
|
||||
</model>
|
|
@ -0,0 +1,12 @@
|
|||
//
|
||||
// https://mczachurski.dev
|
||||
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the Apache License 2.0.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
@objc(ViewedStatus)
|
||||
public class ViewedStatus: NSManagedObject {
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
//
|
||||
// https://mczachurski.dev
|
||||
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the Apache License 2.0.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
extension ViewedStatus {
|
||||
|
||||
@nonobjc public class func fetchRequest() -> NSFetchRequest<ViewedStatus> {
|
||||
return NSFetchRequest<ViewedStatus>(entityName: "ViewedStatus")
|
||||
}
|
||||
|
||||
@NSManaged public var id: String
|
||||
@NSManaged public var reblogId: String?
|
||||
@NSManaged public var date: Date
|
||||
@NSManaged public var pixelfedAccount: AccountData
|
||||
}
|
||||
|
||||
extension ViewedStatus: Identifiable {
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
//
|
||||
// https://mczachurski.dev
|
||||
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the Apache License 2.0.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
import PixelfedKit
|
||||
|
||||
class ViewedStatusHandler {
|
||||
public static let shared = ViewedStatusHandler()
|
||||
private init() { }
|
||||
|
||||
func createViewedStatusEntity(viewContext: NSManagedObjectContext? = nil) -> ViewedStatus {
|
||||
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
|
||||
return ViewedStatus(context: context)
|
||||
}
|
||||
|
||||
/// Check if given status (real picture) has been already visible on the timeline (during last month).
|
||||
func hasBeenAlreadyOnTimeline(accountId: String, status: Status, viewContext: NSManagedObjectContext? = nil) -> Bool {
|
||||
guard let reblog = status.reblog else {
|
||||
return false
|
||||
}
|
||||
|
||||
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
|
||||
let fetchRequest = ViewedStatus.fetchRequest()
|
||||
|
||||
fetchRequest.fetchLimit = 1
|
||||
let statusIdPredicate = NSPredicate(format: "id = %@", reblog.id)
|
||||
let reblogIdPredicate = NSPredicate(format: "reblogId = %@", reblog.id)
|
||||
let idPredicates = NSCompoundPredicate.init(type: .or, subpredicates: [statusIdPredicate, reblogIdPredicate])
|
||||
|
||||
let accountPredicate = NSPredicate(format: "pixelfedAccount.id = %@", accountId)
|
||||
fetchRequest.predicate = NSCompoundPredicate.init(type: .and, subpredicates: [idPredicates, accountPredicate])
|
||||
|
||||
do {
|
||||
guard let first = try context.fetch(fetchRequest).first else {
|
||||
return false
|
||||
}
|
||||
|
||||
if first.reblogId == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
if first.id != status.id {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
} catch {
|
||||
CoreDataError.shared.handle(error, message: "Error during fetching viewed statuses (hasBeenAlreadyOnTimeline).")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/// Mark to delete statuses older then one month.
|
||||
func deleteOldViewedStatuses(viewContext: NSManagedObjectContext? = nil) {
|
||||
let oldViewedStatuses = self.getOldViewedStatuses(viewContext: viewContext)
|
||||
for status in oldViewedStatuses {
|
||||
viewContext?.delete(status)
|
||||
}
|
||||
}
|
||||
|
||||
private func getOldViewedStatuses(viewContext: NSManagedObjectContext? = nil) -> [ViewedStatus] {
|
||||
let context = viewContext ?? CoreDataHandler.shared.container.viewContext
|
||||
|
||||
guard let date = Calendar.current.date(byAdding: .month, value: -1, to: Date()) else {
|
||||
return []
|
||||
}
|
||||
|
||||
do {
|
||||
let fetchRequest = ViewedStatus.fetchRequest()
|
||||
fetchRequest.predicate = NSPredicate(format: "date < %@", date as NSDate)
|
||||
|
||||
return try context.fetch(fetchRequest)
|
||||
} catch {
|
||||
CoreDataError.shared.handle(error, message: "Error during fetching viewed statuses (getOldViewedStatuses).")
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
//
|
||||
// https://mczachurski.dev
|
||||
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the Apache License 2.0.
|
||||
//
|
||||
|
||||
import OSLog
|
||||
|
||||
public extension Logger {
|
||||
/// Using your bundle identifier is a great way to ensure a unique identifier.
|
||||
private static var subsystem = Bundle.main.bundleIdentifier ?? "dev.mczachurski.vernissage"
|
||||
|
||||
/// Logs the main informations.
|
||||
static let main = Logger(subsystem: subsystem, category: "main")
|
||||
}
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
@ -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,48 @@ 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))
|
||||
}
|
||||
|
||||
params.append(("_t", String.randomString(length: 8)))
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,8 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import OSLog
|
||||
import EnvironmentKit
|
||||
import PixelfedKit
|
||||
|
||||
public class ErrorService {
|
||||
|
@ -23,6 +25,7 @@ public class ErrorService {
|
|||
}
|
||||
}
|
||||
|
||||
print("Error ['\(localizedMessage)']: \(error.localizedDescription)")
|
||||
Logger.main.error("Error ['\(localizedMessage)']: \(error.localizedDescription)")
|
||||
Logger.main.error("Error ['\(localizedMessage)']: \(error)")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -194,6 +194,15 @@
|
|||
F8B08862299435C9002AB40A /* SupportView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B08861299435C9002AB40A /* SupportView.swift */; };
|
||||
F8B758DE2AB9DD85000C8068 /* ColumnData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8B758DD2AB9DD85000C8068 /* ColumnData.swift */; };
|
||||
F8D5444329D4066C002225D6 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D5444229D4066C002225D6 /* AppDelegate.swift */; };
|
||||
F8D8E0C72ACC234A00AA1374 /* ViewedStatus+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D8E0C62ACC234A00AA1374 /* ViewedStatus+CoreDataClass.swift */; };
|
||||
F8D8E0C82ACC234A00AA1374 /* ViewedStatus+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D8E0C62ACC234A00AA1374 /* ViewedStatus+CoreDataClass.swift */; };
|
||||
F8D8E0C92ACC234A00AA1374 /* ViewedStatus+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D8E0C62ACC234A00AA1374 /* ViewedStatus+CoreDataClass.swift */; };
|
||||
F8D8E0CB2ACC237000AA1374 /* ViewedStatus+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D8E0CA2ACC237000AA1374 /* ViewedStatus+CoreDataProperties.swift */; };
|
||||
F8D8E0CC2ACC237000AA1374 /* ViewedStatus+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D8E0CA2ACC237000AA1374 /* ViewedStatus+CoreDataProperties.swift */; };
|
||||
F8D8E0CD2ACC237000AA1374 /* ViewedStatus+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D8E0CA2ACC237000AA1374 /* ViewedStatus+CoreDataProperties.swift */; };
|
||||
F8D8E0CF2ACC23B300AA1374 /* ViewedStatusHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D8E0CE2ACC23B300AA1374 /* ViewedStatusHandler.swift */; };
|
||||
F8D8E0D02ACC23B300AA1374 /* ViewedStatusHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D8E0CE2ACC23B300AA1374 /* ViewedStatusHandler.swift */; };
|
||||
F8D8E0D12ACC23B300AA1374 /* ViewedStatusHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8D8E0CE2ACC23B300AA1374 /* ViewedStatusHandler.swift */; };
|
||||
F8DF38E429DD68820047F1AA /* ViewOffsetKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8DF38E329DD68820047F1AA /* ViewOffsetKey.swift */; };
|
||||
F8DF38E629DDB98A0047F1AA /* SocialsSectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8DF38E529DDB98A0047F1AA /* SocialsSectionView.swift */; };
|
||||
F8E36E462AB8745300769C55 /* Sizable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8E36E452AB8745300769C55 /* Sizable.swift */; };
|
||||
|
@ -332,6 +341,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>"; };
|
||||
|
@ -398,9 +408,14 @@
|
|||
F8B3699A29D86EB600BE3808 /* .swiftlint.yml */ = {isa = PBXFileReference; lastKnownFileType = text.yaml; path = .swiftlint.yml; sourceTree = "<group>"; };
|
||||
F8B3699B29D86EBD00BE3808 /* .gitignore */ = {isa = PBXFileReference; lastKnownFileType = text; path = .gitignore; sourceTree = "<group>"; };
|
||||
F8B758DD2AB9DD85000C8068 /* ColumnData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColumnData.swift; sourceTree = "<group>"; };
|
||||
F8BD04192ACC2280004B8E2C /* Vernissage-016.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-016.xcdatamodel"; sourceTree = "<group>"; };
|
||||
F8C937A929882CA90004D782 /* Vernissage-001.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-001.xcdatamodel"; sourceTree = "<group>"; };
|
||||
F8CAE64129B8F1AF001E0372 /* Vernissage-005.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-005.xcdatamodel"; sourceTree = "<group>"; };
|
||||
F8D5444229D4066C002225D6 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
F8D8E0C62ACC234A00AA1374 /* ViewedStatus+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ViewedStatus+CoreDataClass.swift"; sourceTree = "<group>"; };
|
||||
F8D8E0CA2ACC237000AA1374 /* ViewedStatus+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ViewedStatus+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
||||
F8D8E0CE2ACC23B300AA1374 /* ViewedStatusHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewedStatusHandler.swift; sourceTree = "<group>"; };
|
||||
F8D8E0D22ACC89CB00AA1374 /* Vernissage-017.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Vernissage-017.xcdatamodel"; sourceTree = "<group>"; };
|
||||
F8DF38E329DD68820047F1AA /* ViewOffsetKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewOffsetKey.swift; sourceTree = "<group>"; };
|
||||
F8DF38E529DDB98A0047F1AA /* SocialsSectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocialsSectionView.swift; sourceTree = "<group>"; };
|
||||
F8DF38E729DDC3D20047F1AA /* eu */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = eu; path = eu.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
|
@ -569,11 +584,14 @@
|
|||
F88FAD28295F43B8009B20C9 /* AccountData+CoreDataClass.swift */,
|
||||
F88FAD29295F43B8009B20C9 /* AccountData+CoreDataProperties.swift */,
|
||||
F88BC51C29E0377B00CE6141 /* AccountData+AccountModel.swift */,
|
||||
F8D8E0C62ACC234A00AA1374 /* ViewedStatus+CoreDataClass.swift */,
|
||||
F8D8E0CA2ACC237000AA1374 /* ViewedStatus+CoreDataProperties.swift */,
|
||||
F88C2474295C37BB0006098B /* CoreDataHandler.swift */,
|
||||
F866F6A229604161002E8F88 /* AccountDataHandler.swift */,
|
||||
F866F6A429604194002E8F88 /* ApplicationSettingsHandler.swift */,
|
||||
F80048072961E6DE00E6868A /* StatusDataHandler.swift */,
|
||||
F80048092961EA1900E6868A /* AttachmentDataHandler.swift */,
|
||||
F8D8E0CE2ACC23B300AA1374 /* ViewedStatusHandler.swift */,
|
||||
F864F7A429BBA01D00B13921 /* CoreDataError.swift */,
|
||||
);
|
||||
path = CoreData;
|
||||
|
@ -1098,6 +1116,7 @@
|
|||
files = (
|
||||
F864F77829BB930000B13921 /* PhotoWidgetEntry.swift in Sources */,
|
||||
F864F77529BB92CE00B13921 /* PhotoProvider.swift in Sources */,
|
||||
F8D8E0D02ACC23B300AA1374 /* ViewedStatusHandler.swift in Sources */,
|
||||
F864F77629BB92CE00B13921 /* PhotoWidgetEntryView.swift in Sources */,
|
||||
F8705A7729FF7ABD00DA818A /* QRCodeSmallWidgetView.swift in Sources */,
|
||||
F864F77C29BB982100B13921 /* StatusFetcher.swift in Sources */,
|
||||
|
@ -1110,6 +1129,8 @@
|
|||
F8F6E44E29BCC1FB0004795E /* PhotoLargeWidgetView.swift in Sources */,
|
||||
F864F76429BB91B400B13921 /* VernissageWidgetBundle.swift in Sources */,
|
||||
F864F77D29BB9A4600B13921 /* AttachmentData+CoreDataClass.swift in Sources */,
|
||||
F8D8E0C82ACC234A00AA1374 /* ViewedStatus+CoreDataClass.swift in Sources */,
|
||||
F8D8E0CC2ACC237000AA1374 /* ViewedStatus+CoreDataProperties.swift in Sources */,
|
||||
F864F7A629BBA01D00B13921 /* CoreDataError.swift in Sources */,
|
||||
F864F77E29BB9A4900B13921 /* AttachmentData+CoreDataProperties.swift in Sources */,
|
||||
F864F78229BB9A6500B13921 /* StatusData+CoreDataClass.swift in Sources */,
|
||||
|
@ -1144,6 +1165,7 @@
|
|||
files = (
|
||||
F88BC54529E072B200CE6141 /* AccountDataHandler.swift in Sources */,
|
||||
F88BC54729E072B800CE6141 /* AccountData+CoreDataProperties.swift in Sources */,
|
||||
F8D8E0D12ACC23B300AA1374 /* ViewedStatusHandler.swift in Sources */,
|
||||
F88BC54D29E072D600CE6141 /* AttachmentData+CoreDataProperties.swift in Sources */,
|
||||
F88BC54F29E073BC00CE6141 /* AccountData+AccountModel.swift in Sources */,
|
||||
F865B4D32A024AFE008ACDFC /* AttachmentData+Faulty.swift in Sources */,
|
||||
|
@ -1152,10 +1174,12 @@
|
|||
F88BC54129E072A600CE6141 /* CoreDataError.swift in Sources */,
|
||||
F88BC54229E072A900CE6141 /* AttachmentDataHandler.swift in Sources */,
|
||||
F88BC54429E072AF00CE6141 /* ApplicationSettingsHandler.swift in Sources */,
|
||||
F8D8E0CD2ACC237000AA1374 /* ViewedStatus+CoreDataProperties.swift in Sources */,
|
||||
F88BC51629E0307F00CE6141 /* NotificationsName.swift in Sources */,
|
||||
F88BC54829E072BC00CE6141 /* AccountData+CoreDataClass.swift in Sources */,
|
||||
F88BC51329E02FD800CE6141 /* ComposeView.swift in Sources */,
|
||||
F88BC54E29E072D900CE6141 /* AttachmentData+CoreDataClass.swift in Sources */,
|
||||
F8D8E0C92ACC234A00AA1374 /* ViewedStatus+CoreDataClass.swift in Sources */,
|
||||
F88BC54C29E072CD00CE6141 /* StatusData+CoreDataClass.swift in Sources */,
|
||||
F88BC54B29E072CA00CE6141 /* StatusData+CoreDataProperties.swift in Sources */,
|
||||
F88BC54A29E072C400CE6141 /* ApplicationSettings+CoreDataClass.swift in Sources */,
|
||||
|
@ -1198,6 +1222,7 @@
|
|||
F805DCF129DBEF83006A1FD9 /* ReportView.swift in Sources */,
|
||||
F8B0886029943498002AB40A /* OtherSectionView.swift in Sources */,
|
||||
F808641429756666009F035C /* NotificationRowView.swift in Sources */,
|
||||
F8D8E0C72ACC234A00AA1374 /* ViewedStatus+CoreDataClass.swift in Sources */,
|
||||
F8624D3D29F2D3AC00204986 /* SelectedMenuItemDetails.swift in Sources */,
|
||||
F8210DDD2966CF17001D9973 /* StatusData+Status.swift in Sources */,
|
||||
F8210DCF2966B600001D9973 /* ImageRowAsync.swift in Sources */,
|
||||
|
@ -1252,6 +1277,7 @@
|
|||
F802884F297AEED5000BDD51 /* DatabaseError.swift in Sources */,
|
||||
F86A4307299AA5E900DF7645 /* ThanksView.swift in Sources */,
|
||||
F8FB8ABA29EB2ED400342C04 /* NavigationMenuButtons.swift in Sources */,
|
||||
F8D8E0CF2ACC23B300AA1374 /* ViewedStatusHandler.swift in Sources */,
|
||||
F88BC51D29E0377B00CE6141 /* AccountData+AccountModel.swift in Sources */,
|
||||
F89B5CC229D01BF700549F2F /* InstanceView.swift in Sources */,
|
||||
F825F0CB29F7CFC4008BD204 /* FollowRequestsView.swift in Sources */,
|
||||
|
@ -1264,6 +1290,7 @@
|
|||
F88E4D56297EAD6E0057491A /* AppRouteur.swift in Sources */,
|
||||
F88FAD27295F400E009B20C9 /* NotificationsView.swift in Sources */,
|
||||
F86B7216296BFFDA00EE59EC /* UserProfileStatusesView.swift in Sources */,
|
||||
F8D8E0CB2ACC237000AA1374 /* ViewedStatus+CoreDataProperties.swift in Sources */,
|
||||
F897978F29684BCB00B22335 /* LoadingView.swift in Sources */,
|
||||
F89992C9296D6DC7005994BF /* CommentBodyView.swift in Sources */,
|
||||
F866F6A1296040A8002E8F88 /* ApplicationSettings+CoreDataProperties.swift in Sources */,
|
||||
|
@ -1321,7 +1348,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = VernissageWidget/VernissageWidgetExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 215;
|
||||
CURRENT_PROJECT_VERSION = 256;
|
||||
DEVELOPMENT_TEAM = B2U9FEKYP8;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = VernissageWidget/Info.plist;
|
||||
|
@ -1333,7 +1360,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.11.0;
|
||||
MARKETING_VERSION = 1.12.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage.widget;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
|
@ -1352,7 +1379,7 @@
|
|||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||
CODE_SIGN_ENTITLEMENTS = VernissageWidget/VernissageWidgetExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 215;
|
||||
CURRENT_PROJECT_VERSION = 256;
|
||||
DEVELOPMENT_TEAM = B2U9FEKYP8;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = VernissageWidget/Info.plist;
|
||||
|
@ -1364,7 +1391,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.11.0;
|
||||
MARKETING_VERSION = 1.12.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage.widget;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
|
@ -1382,7 +1409,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = VernissageShare/VernissageShareExtension.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 215;
|
||||
CURRENT_PROJECT_VERSION = 256;
|
||||
DEVELOPMENT_TEAM = B2U9FEKYP8;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = VernissageShare/Info.plist;
|
||||
|
@ -1394,7 +1421,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.11.0;
|
||||
MARKETING_VERSION = 1.12.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage.share;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
|
@ -1411,7 +1438,7 @@
|
|||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = NO;
|
||||
CODE_SIGN_ENTITLEMENTS = VernissageShare/VernissageShareExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 215;
|
||||
CURRENT_PROJECT_VERSION = 256;
|
||||
DEVELOPMENT_TEAM = B2U9FEKYP8;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = VernissageShare/Info.plist;
|
||||
|
@ -1423,7 +1450,7 @@
|
|||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.11.0;
|
||||
MARKETING_VERSION = 1.12.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage.share;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
|
@ -1557,7 +1584,7 @@
|
|||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "Violet Blue Pride Pride-Camera Blue-Camera Violet-Camera Orange-Camera Orange Yellow-Camera Yellow Gradient-Camera Gradient";
|
||||
ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "Violet Blue Pride Pride-Camera Blue-Camera Violet-Camera Orange-Camera Orange Yellow-Camera Yellow Gradient-Camera Gradient Brown-Lens Pink-Lens Blue-Lens Orange-Lens";
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = Default;
|
||||
ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOLS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = NO;
|
||||
|
@ -1565,7 +1592,7 @@
|
|||
CODE_SIGN_ENTITLEMENTS = Vernissage/Vernissage.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 215;
|
||||
CURRENT_PROJECT_VERSION = 256;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"Vernissage/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = B2U9FEKYP8;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
|
@ -1584,13 +1611,13 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.11.0;
|
||||
MARKETING_VERSION = 1.12.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
|
@ -1601,14 +1628,14 @@
|
|||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "Violet Blue Pride Pride-Camera Blue-Camera Violet-Camera Orange-Camera Orange Yellow-Camera Yellow Gradient-Camera Gradient";
|
||||
ASSETCATALOG_COMPILER_ALTERNATE_APPICON_NAMES = "Violet Blue Pride Pride-Camera Blue-Camera Violet-Camera Orange-Camera Orange Yellow-Camera Yellow Gradient-Camera Gradient Brown-Lens Pink-Lens Blue-Lens Orange-Lens";
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = Default;
|
||||
ASSETCATALOG_COMPILER_GENERATE_ASSET_SYMBOLS = NO;
|
||||
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = NO;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = Vernissage/Vernissage.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 215;
|
||||
CURRENT_PROJECT_VERSION = 256;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"Vernissage/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = B2U9FEKYP8;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
|
@ -1627,12 +1654,12 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.11.0;
|
||||
MARKETING_VERSION = 1.12.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES;
|
||||
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
|
@ -1805,6 +1832,9 @@
|
|||
F88C2476295C37BB0006098B /* Vernissage.xcdatamodeld */ = {
|
||||
isa = XCVersionGroup;
|
||||
children = (
|
||||
F8D8E0D22ACC89CB00AA1374 /* Vernissage-017.xcdatamodel */,
|
||||
F8BD04192ACC2280004B8E2C /* Vernissage-016.xcdatamodel */,
|
||||
F880EECE2AC70A2B00C09C31 /* Vernissage-015.xcdatamodel */,
|
||||
F8206A032A06547600E19412 /* Vernissage-014.xcdatamodel */,
|
||||
F865B4D42A0252FB008ACDFC /* Vernissage-013.xcdatamodel */,
|
||||
F8EF3C8B29FC3A5F00CBFF7C /* Vernissage-012.xcdatamodel */,
|
||||
|
@ -1821,7 +1851,7 @@
|
|||
F8C937A929882CA90004D782 /* Vernissage-001.xcdatamodel */,
|
||||
F88C2477295C37BB0006098B /* Vernissage.xcdatamodel */,
|
||||
);
|
||||
currentVersion = F8206A032A06547600E19412 /* Vernissage-014.xcdatamodel */;
|
||||
currentVersion = F8D8E0D22ACC89CB00AA1374 /* Vernissage-017.xcdatamodel */;
|
||||
path = Vernissage.xcdatamodeld;
|
||||
sourceTree = "<group>";
|
||||
versionGroupType = wrapper.xcdatamodel;
|
||||
|
|
BIN
Vernissage/Assets.xcassets/AppIcons/Blue-Lens-Preview.imageset/Blue-Lens-Preview.png
vendored
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
Vernissage/Assets.xcassets/AppIcons/Blue-Lens-Preview.imageset/Blue-Lens-Preview@2x.png
vendored
Normal file
After Width: | Height: | Size: 6.5 KiB |
BIN
Vernissage/Assets.xcassets/AppIcons/Blue-Lens-Preview.imageset/Blue-Lens-Preview@3x.png
vendored
Normal file
After Width: | Height: | Size: 9.7 KiB |
23
Vernissage/Assets.xcassets/AppIcons/Blue-Lens-Preview.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Blue-Lens-Preview.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Blue-Lens-Preview@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Blue-Lens-Preview@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 543 KiB |
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "BlueLens.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
Vernissage/Assets.xcassets/AppIcons/Brown-Lens-Preview.imageset/Brown-Lens-Preview.png
vendored
Normal file
After Width: | Height: | Size: 4.0 KiB |
BIN
Vernissage/Assets.xcassets/AppIcons/Brown-Lens-Preview.imageset/Brown-Lens-Preview@2x.png
vendored
Normal file
After Width: | Height: | Size: 6.8 KiB |
BIN
Vernissage/Assets.xcassets/AppIcons/Brown-Lens-Preview.imageset/Brown-Lens-Preview@3x.png
vendored
Normal file
After Width: | Height: | Size: 10 KiB |
23
Vernissage/Assets.xcassets/AppIcons/Brown-Lens-Preview.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Brown-Lens-Preview.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Brown-Lens-Preview@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Brown-Lens-Preview@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 438 KiB |
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "BrownLens.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
23
Vernissage/Assets.xcassets/AppIcons/Orange-Lens-Preview.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Lens-Preview.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Lens-Preview@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Lens-Preview@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
Vernissage/Assets.xcassets/AppIcons/Orange-Lens-Preview.imageset/Lens-Preview.png
vendored
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
Vernissage/Assets.xcassets/AppIcons/Orange-Lens-Preview.imageset/Lens-Preview@2x.png
vendored
Normal file
After Width: | Height: | Size: 6.3 KiB |
BIN
Vernissage/Assets.xcassets/AppIcons/Orange-Lens-Preview.imageset/Lens-Preview@3x.png
vendored
Normal file
After Width: | Height: | Size: 9.0 KiB |
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Lens.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 406 KiB |
23
Vernissage/Assets.xcassets/AppIcons/Pink-Lens-Preview.imageset/Contents.json
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Pink-Lens-Preview.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "Pink-Lens-Preview@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "Pink-Lens-Preview@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
BIN
Vernissage/Assets.xcassets/AppIcons/Pink-Lens-Preview.imageset/Pink-Lens-Preview.png
vendored
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
Vernissage/Assets.xcassets/AppIcons/Pink-Lens-Preview.imageset/Pink-Lens-Preview@2x.png
vendored
Normal file
After Width: | Height: | Size: 6.5 KiB |
BIN
Vernissage/Assets.xcassets/AppIcons/Pink-Lens-Preview.imageset/Pink-Lens-Preview@3x.png
vendored
Normal file
After Width: | Height: | Size: 9.8 KiB |
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "PinkLens.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 470 KiB |
|
@ -7,6 +7,8 @@
|
|||
import Foundation
|
||||
import StoreKit
|
||||
import ServicesKit
|
||||
import OSLog
|
||||
import EnvironmentKit
|
||||
|
||||
@MainActor
|
||||
final class TipsStore: ObservableObject {
|
||||
|
@ -78,9 +80,9 @@ final class TipsStore: ObservableObject {
|
|||
self.status = .successful
|
||||
await transaction.finish()
|
||||
case .userCancelled:
|
||||
print("User click cancel before their transaction started.")
|
||||
Logger.main.warning("User click cancel before their transaction started.")
|
||||
case .pending:
|
||||
print("User needs to complete some action on their account before their complete the purchase.")
|
||||
Logger.main.warning("User needs to complete some action on their account before their complete the purchase.")
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
|
|
@ -10,6 +10,9 @@ import PixelfedKit
|
|||
import ClientKit
|
||||
import ServicesKit
|
||||
import Nuke
|
||||
import OSLog
|
||||
import EnvironmentKit
|
||||
import Semaphore
|
||||
|
||||
/// Service responsible for managing home timeline.
|
||||
public class HomeTimelineService {
|
||||
|
@ -18,9 +21,10 @@ public class HomeTimelineService {
|
|||
|
||||
private let defaultAmountOfDownloadedStatuses = 40
|
||||
private let imagePrefetcher = ImagePrefetcher(destination: .diskCache)
|
||||
private let semaphore = AsyncSemaphore(value: 1)
|
||||
|
||||
@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 +36,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,23 +49,29 @@ 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? {
|
||||
await semaphore.wait()
|
||||
defer { semaphore.signal() }
|
||||
|
||||
// Load data from API and operate on CoreData on background context.
|
||||
let backgroundContext = CoreDataHandler.shared.newBackgroundContext()
|
||||
|
||||
// Retrieve newest visible status (last visible by user).
|
||||
let dbNewestStatus = StatusDataHandler.shared.getMaximumStatus(accountId: account.id, viewContext: backgroundContext)
|
||||
let lastSeenStatusId = dbNewestStatus?.rebloggedStatusId ?? dbNewestStatus?.id
|
||||
let lastSeenStatusId = dbNewestStatus?.id
|
||||
|
||||
// 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 {
|
||||
try self.update(lastSeenStatusId: lastSeenStatusId, for: account, on: backgroundContext)
|
||||
}
|
||||
|
||||
// Delete old viewed statuses from database.
|
||||
ViewedStatusHandler.shared.deleteOldViewedStatuses(viewContext: backgroundContext)
|
||||
|
||||
// Start prefetching images.
|
||||
self.prefetch(statuses: allStatusesFromApi)
|
||||
|
||||
|
@ -72,28 +82,6 @@ public class HomeTimelineService {
|
|||
return lastSeenStatusId
|
||||
}
|
||||
|
||||
private func update(lastSeenStatusId: String, for account: AccountModel, on backgroundContext: NSManagedObjectContext) throws {
|
||||
// Save information about last seen status.
|
||||
guard let accountDataFromDb = AccountDataHandler.shared.getAccountData(accountId: account.id, viewContext: backgroundContext) else {
|
||||
throw DatabaseError.cannotDownloadAccount
|
||||
}
|
||||
|
||||
accountDataFromDb.lastSeenStatusId = lastSeenStatusId
|
||||
}
|
||||
|
||||
public func update(status statusData: StatusData, basedOn status: Status, for account: AccountModel) async throws -> StatusData? {
|
||||
// Load data from API and operate on CoreData on background context.
|
||||
let backgroundContext = CoreDataHandler.shared.newBackgroundContext()
|
||||
|
||||
// Update status data in database.
|
||||
self.copy(from: status, to: statusData, on: backgroundContext)
|
||||
|
||||
// Save data into database.
|
||||
CoreDataHandler.shared.save(viewContext: backgroundContext)
|
||||
|
||||
return statusData
|
||||
}
|
||||
|
||||
@MainActor
|
||||
public func update(attachment: AttachmentData, withData imageData: Data, imageWidth: Double, imageHeight: Double) {
|
||||
attachment.data = imageData
|
||||
|
@ -107,7 +95,10 @@ public class HomeTimelineService {
|
|||
CoreDataHandler.shared.save()
|
||||
}
|
||||
|
||||
public func amountOfNewStatuses(for account: AccountModel) async -> Int {
|
||||
public func amountOfNewStatuses(for account: AccountModel, includeReblogs: Bool) async -> Int {
|
||||
await semaphore.wait()
|
||||
defer { semaphore.signal() }
|
||||
|
||||
guard let accessToken = account.accessToken else {
|
||||
return 0
|
||||
}
|
||||
|
@ -122,13 +113,16 @@ public class HomeTimelineService {
|
|||
}
|
||||
|
||||
let client = PixelfedClient(baseURL: account.serverUrl).getAuthenticated(token: accessToken)
|
||||
var amountOfStatuses = 0
|
||||
var statuses: [Status] = []
|
||||
var newestStatusId = newestStatus.id
|
||||
|
||||
// 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
|
||||
}
|
||||
|
@ -136,7 +130,25 @@ public class HomeTimelineService {
|
|||
// We have to include in the counter only statuses with images.
|
||||
let statusesWithImagesOnly = downloadedStatuses.getStatusesWithImagesOnly()
|
||||
|
||||
amountOfStatuses = amountOfStatuses + statusesWithImagesOnly.count
|
||||
for status in statusesWithImagesOnly {
|
||||
// We should add to timeline only statuses that has not been showned to the user already.
|
||||
guard self.hasBeenAlreadyOnTimeline(accountId: account.id, status: status, on: backgroundContext) == false else {
|
||||
continue
|
||||
}
|
||||
|
||||
// Same rebloged status has been already visible in current portion of data.
|
||||
if let reblog = status.reblog, statuses.contains(where: { $0.reblog?.id == reblog.id }) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Same status has been already visible in current portion of data.
|
||||
if let reblog = status.reblog, statusesWithImagesOnly.contains(where: { $0.id == reblog.id }) {
|
||||
continue
|
||||
}
|
||||
|
||||
statuses.append(status)
|
||||
}
|
||||
|
||||
newestStatusId = firstStatus.id
|
||||
} catch {
|
||||
ErrorService.shared.handle(error, message: "Error during downloading new statuses for amount of new statuses.")
|
||||
|
@ -144,17 +156,38 @@ public class HomeTimelineService {
|
|||
}
|
||||
}
|
||||
|
||||
return amountOfStatuses
|
||||
// Start prefetching images.
|
||||
self.prefetch(statuses: statuses)
|
||||
|
||||
// Return number of new statuses not visible yet on the timeline.
|
||||
return statuses.count
|
||||
}
|
||||
|
||||
private func refresh(for account: AccountModel, on backgroundContext: NSManagedObjectContext) async throws -> [Status] {
|
||||
guard let accessToken = account.accessToken else {
|
||||
return []
|
||||
private func update(lastSeenStatusId: String, for account: AccountModel, on backgroundContext: NSManagedObjectContext) throws {
|
||||
// Save information about last seen status.
|
||||
guard let accountDataFromDb = AccountDataHandler.shared.getAccountData(accountId: account.id, viewContext: backgroundContext) else {
|
||||
throw DatabaseError.cannotDownloadAccount
|
||||
}
|
||||
|
||||
accountDataFromDb.lastSeenStatusId = lastSeenStatusId
|
||||
}
|
||||
|
||||
private func update(status statusData: StatusData, basedOn status: Status, for account: AccountModel) async throws -> StatusData? {
|
||||
// Load data from API and operate on CoreData on background context.
|
||||
let backgroundContext = CoreDataHandler.shared.newBackgroundContext()
|
||||
|
||||
// Update status data in database.
|
||||
self.copy(from: status, to: statusData, on: backgroundContext)
|
||||
|
||||
// Save data into database.
|
||||
CoreDataHandler.shared.save(viewContext: backgroundContext)
|
||||
|
||||
return statusData
|
||||
}
|
||||
|
||||
private func refresh(for account: AccountModel, includeReblogs: Bool, on backgroundContext: NSManagedObjectContext) async throws -> [Status] {
|
||||
// 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 self.getUniqueStatusesForHomeTimeline(account: account, includeReblogs: includeReblogs, on: backgroundContext)
|
||||
|
||||
// Update all existing statuses in database.
|
||||
for status in statuses {
|
||||
|
@ -207,17 +240,12 @@ public class HomeTimelineService {
|
|||
}
|
||||
|
||||
private func load(for account: AccountModel,
|
||||
includeReblogs: Bool,
|
||||
on backgroundContext: NSManagedObjectContext,
|
||||
minId: String? = nil,
|
||||
maxId: String? = nil
|
||||
) 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(maxId: maxId, minId: minId, limit: self.defaultAmountOfDownloadedStatuses)
|
||||
let statuses = try await self.getUniqueStatusesForHomeTimeline(account: account, maxId: maxId, includeReblogs: includeReblogs, on: backgroundContext)
|
||||
|
||||
// Save statuses in database.
|
||||
try await self.add(statuses, for: account, on: backgroundContext)
|
||||
|
@ -238,14 +266,23 @@ public class HomeTimelineService {
|
|||
// Proceed statuses with images only.
|
||||
let statusesWithImages = statuses.getStatusesWithImagesOnly()
|
||||
|
||||
// Save status data in database.
|
||||
// Save all data to database.
|
||||
for status in statusesWithImages {
|
||||
// Save status to database.
|
||||
let statusData = StatusDataHandler.shared.createStatusDataEntity(viewContext: backgroundContext)
|
||||
self.copy(from: status, to: statusData, on: backgroundContext)
|
||||
|
||||
statusData.pixelfedAccount = accountDataFromDb
|
||||
accountDataFromDb.addToStatuses(statusData)
|
||||
|
||||
self.copy(from: status, to: statusData, on: backgroundContext)
|
||||
// Save statusId to viewed statuses.
|
||||
let viewedStatus = ViewedStatusHandler.shared.createViewedStatusEntity(viewContext: backgroundContext)
|
||||
|
||||
viewedStatus.id = status.id
|
||||
viewedStatus.reblogId = status.reblog?.id
|
||||
viewedStatus.date = Date()
|
||||
viewedStatus.pixelfedAccount = accountDataFromDb
|
||||
accountDataFromDb.addToViewedStatuses(viewedStatus)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -298,7 +335,62 @@ public class HomeTimelineService {
|
|||
}
|
||||
|
||||
private func prefetch(statuses: [Status]) {
|
||||
let statusModels = statuses.getStatusesWithImagesOnly().toStatusModels()
|
||||
let statusModels = statuses.toStatusModels()
|
||||
imagePrefetcher.startPrefetching(with: statusModels.getAllImagesUrls())
|
||||
}
|
||||
|
||||
private func hasBeenAlreadyOnTimeline(accountId: String, status: Status, on backgroundContext: NSManagedObjectContext) -> Bool {
|
||||
return ViewedStatusHandler.shared.hasBeenAlreadyOnTimeline(accountId: accountId, status: status, viewContext: backgroundContext)
|
||||
}
|
||||
|
||||
private func getUniqueStatusesForHomeTimeline(account: AccountModel, maxId: EntityId? = nil, includeReblogs: Bool? = nil, on backgroundContext: NSManagedObjectContext) async throws -> [Status] {
|
||||
guard let accessToken = account.accessToken else {
|
||||
return []
|
||||
}
|
||||
|
||||
let client = PixelfedClient(baseURL: account.serverUrl).getAuthenticated(token: accessToken)
|
||||
var lastStatusId = maxId
|
||||
var statuses: [Status] = []
|
||||
|
||||
while true {
|
||||
let downloadedStatuses = try await client.getHomeTimeline(maxId: lastStatusId,
|
||||
limit: self.defaultAmountOfDownloadedStatuses,
|
||||
includeReblogs: includeReblogs)
|
||||
|
||||
// When there is not any older statuses we have to finish.
|
||||
guard let lastStatus = downloadedStatuses.last else {
|
||||
break
|
||||
}
|
||||
|
||||
// We have to include in the counter only statuses with images.
|
||||
let statusesWithImagesOnly = downloadedStatuses.getStatusesWithImagesOnly()
|
||||
|
||||
for status in statusesWithImagesOnly {
|
||||
// We should add to timeline only statuses that has not been showned to the user already.
|
||||
guard self.hasBeenAlreadyOnTimeline(accountId: account.id, status: status, on: backgroundContext) == false else {
|
||||
continue
|
||||
}
|
||||
|
||||
// Same rebloged status has been already visible in current portion of data.
|
||||
if let reblog = status.reblog, statuses.contains(where: { $0.reblog?.id == reblog.id }) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Same status has been already visible in current portion of data.
|
||||
if let reblog = status.reblog, statusesWithImagesOnly.contains(where: { $0.id == reblog.id }) {
|
||||
continue
|
||||
}
|
||||
|
||||
statuses.append(status)
|
||||
}
|
||||
|
||||
if statuses.count >= self.defaultAmountOfDownloadedStatuses {
|
||||
break
|
||||
}
|
||||
|
||||
lastStatusId = lastStatus.id
|
||||
}
|
||||
|
||||
return statuses
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,11 +11,25 @@ import ServicesKit
|
|||
|
||||
public extension View {
|
||||
func imageContextMenu(statusModel: StatusModel, attachmentModel: AttachmentModel, uiImage: UIImage?) -> some View {
|
||||
modifier(ImageContextMenu(id: statusModel.id, url: statusModel.url, altText: attachmentModel.description, uiImage: uiImage))
|
||||
modifier(
|
||||
ImageContextMenu(
|
||||
id: statusModel.getOrginalStatusId(),
|
||||
url: statusModel.url,
|
||||
altText: attachmentModel.description,
|
||||
uiImage: uiImage
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func imageContextMenu(statusData: StatusData, attachmentData: AttachmentData, uiImage: UIImage?) -> some View {
|
||||
modifier(ImageContextMenu(id: statusData.id, url: statusData.url, altText: attachmentData.text, uiImage: uiImage))
|
||||
modifier(
|
||||
ImageContextMenu(
|
||||
id: statusData.getOrginalStatusId(),
|
||||
url: statusData.url,
|
||||
altText: attachmentData.text,
|
||||
uiImage: uiImage
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,8 @@ import SwiftUI
|
|||
import ServicesKit
|
||||
import EnvironmentKit
|
||||
import WidgetsKit
|
||||
import OSLog
|
||||
import Semaphore
|
||||
|
||||
struct HomeFeedView: View {
|
||||
@Environment(\.managedObjectContext) private var viewContext
|
||||
|
@ -69,7 +71,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,9 +103,8 @@ 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)
|
||||
|
||||
asyncAfter(0.35) {
|
||||
let lastSeenStatusId = try await HomeTimelineService.shared.refreshTimeline(for: account, includeReblogs: self.applicationState.showReboostedStatuses, updateLastSeenStatus: true)
|
||||
asyncAfter(0.75) {
|
||||
self.applicationState.lastSeenStatusId = lastSeenStatusId
|
||||
self.applicationState.amountOfNewStatuses = 0
|
||||
}
|
||||
|
@ -125,7 +126,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
|
||||
|
@ -173,7 +174,7 @@ struct HomeFeedView: View {
|
|||
.resizable()
|
||||
.frame(width: 64, height: 64)
|
||||
.fontWeight(.ultraLight)
|
||||
.foregroundColor(.accentColor.opacity(0.6))
|
||||
.foregroundColor(self.applicationState.tintColor.color().opacity(0.6))
|
||||
Text("home.title.allCaughtUp", comment: "You're all caught up")
|
||||
.font(.title2)
|
||||
.fontWeight(.thin)
|
||||
|
|
|
@ -81,7 +81,7 @@ struct PaginableStatusesView: View {
|
|||
private func list() -> some View {
|
||||
ScrollView {
|
||||
if self.imageColumns > 1 {
|
||||
WaterfallGrid($statusViewModels, columns: $imageColumns, hideLoadMore: $allItemsLoaded) { item in
|
||||
WaterfallGrid($statusViewModels, refreshId: Binding.constant(""), columns: $imageColumns, hideLoadMore: $allItemsLoaded) { item in
|
||||
ImageRowAsync(statusViewModel: item, containerWidth: $containerWidth)
|
||||
} onLoadMore: {
|
||||
do {
|
||||
|
|
|
@ -22,7 +22,11 @@ struct GeneralSectionView: View {
|
|||
"Orange-Camera",
|
||||
"Pride-Camera",
|
||||
"Yellow-Camera",
|
||||
"Gradient-Camera"]
|
||||
"Gradient-Camera",
|
||||
"Orange-Lens",
|
||||
"Pink-Lens",
|
||||
"Blue-Lens",
|
||||
"Brown-Lens"]
|
||||
|
||||
private let themeNames: [(theme: Theme, name: LocalizedStringKey)] = [
|
||||
(Theme.system, "settings.title.system"),
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -162,7 +162,7 @@ struct StatusView: View {
|
|||
}
|
||||
.padding(8)
|
||||
|
||||
CommentsSectionView(statusId: statusViewModel.id)
|
||||
CommentsSectionView(statusId: statusViewModel.getOrginalStatusId())
|
||||
}
|
||||
}
|
||||
.coordinateSpace(name: "scroll")
|
||||
|
|
|
@ -52,6 +52,7 @@ struct StatusesView: View {
|
|||
@State private var statusViewModels: [StatusModel] = []
|
||||
@State private var state: ViewState = .loading
|
||||
@State private var lastStatusId: String?
|
||||
@State private var waterfallId: String = String.randomString(length: 8)
|
||||
|
||||
// Gallery parameters.
|
||||
@State private var imageColumns = 3
|
||||
|
@ -96,7 +97,7 @@ struct StatusesView: View {
|
|||
private func list() -> some View {
|
||||
ScrollView {
|
||||
if self.imageColumns > 1 {
|
||||
WaterfallGrid($statusViewModels, columns: $imageColumns, hideLoadMore: $allItemsLoaded) { item in
|
||||
WaterfallGrid($statusViewModels, refreshId: $waterfallId, columns: $imageColumns, hideLoadMore: $allItemsLoaded) { item in
|
||||
ImageRowAsync(statusViewModel: item, containerWidth: $containerWidth)
|
||||
} onLoadMore: {
|
||||
do {
|
||||
|
@ -142,6 +143,17 @@ struct StatusesView: View {
|
|||
ErrorService.shared.handle(error, message: "statuses.error.loadingStatusesFailed", showToastr: !Task.isCancelled)
|
||||
}
|
||||
}
|
||||
.onChange(of: self.applicationState.showReboostedStatuses) { _ in
|
||||
if self.listType != .home {
|
||||
return
|
||||
}
|
||||
|
||||
Task { @MainActor in
|
||||
HapticService.shared.fireHaptic(of: .dataRefresh(intensity: 0.3))
|
||||
try await self.loadTopStatuses()
|
||||
HapticService.shared.fireHaptic(of: .dataRefresh(intensity: 0.7))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func loadData() async {
|
||||
|
@ -228,7 +240,11 @@ struct StatusesView: View {
|
|||
inPlaceStatuses.append(StatusModel(status: item))
|
||||
}
|
||||
|
||||
// Prefetch images.
|
||||
self.prefetch(statusModels: inPlaceStatuses)
|
||||
|
||||
// Replace old collection with new one.
|
||||
self.waterfallId = String.randomString(length: 8)
|
||||
self.statusViewModels = inPlaceStatuses
|
||||
}
|
||||
|
||||
|
@ -239,7 +255,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,
|
||||
|
|
|
@ -90,7 +90,7 @@ struct TrendStatusesView: View {
|
|||
NoDataView(imageSystemName: "photo.on.rectangle.angled", text: "trendingStatuses.title.noPhotos")
|
||||
} else {
|
||||
if self.imageColumns > 1 {
|
||||
WaterfallGrid($statusViewModels, columns: $imageColumns, hideLoadMore: Binding.constant(true)) { item in
|
||||
WaterfallGrid($statusViewModels, refreshId: Binding.constant(""), columns: $imageColumns, hideLoadMore: Binding.constant(true)) { item in
|
||||
ImageRowAsync(statusViewModel: item, containerWidth: $containerWidth)
|
||||
} onLoadMore: { }
|
||||
} else {
|
||||
|
|
|
@ -42,7 +42,7 @@ struct UserProfileStatusesView: View {
|
|||
var body: some View {
|
||||
if firstLoadFinished == true {
|
||||
if self.imageColumns > 1 {
|
||||
WaterfallGrid($statusViewModels, columns: $imageColumns, hideLoadMore: $allItemsLoaded) { item in
|
||||
WaterfallGrid($statusViewModels, refreshId: Binding.constant(""), columns: $imageColumns, hideLoadMore: $allItemsLoaded) { item in
|
||||
ImageRowAsync(statusViewModel: item, withAvatar: false, containerWidth: $containerWidth)
|
||||
} onLoadMore: {
|
||||
do {
|
||||
|
|
|
@ -61,10 +61,22 @@ 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) { isAuthor in
|
||||
if isAuthor {
|
||||
self.routerPath.navigate(to: .userProfile(accountId: self.status.accountId,
|
||||
accountDisplayName: self.status.accountDisplayName,
|
||||
accountUserName: self.status.accountUsername))
|
||||
} else {
|
||||
if let rebloggedAccountId = self.status.rebloggedAccountId,
|
||||
let rebloggedAccountUsername = self.status.rebloggedAccountUsername {
|
||||
self.routerPath.navigate(to: .userProfile(accountId: rebloggedAccountId,
|
||||
accountDisplayName: self.status.rebloggedAccountDisplayName,
|
||||
accountUserName: rebloggedAccountUsername))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
|
@ -141,10 +153,22 @@ 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) { isAuthor in
|
||||
if isAuthor {
|
||||
self.routerPath.navigate(to: .userProfile(accountId: self.status.accountId,
|
||||
accountDisplayName: self.status.accountDisplayName,
|
||||
accountUserName: self.status.accountUsername))
|
||||
} else {
|
||||
if let rebloggedAccountId = self.status.rebloggedAccountId,
|
||||
let rebloggedAccountUsername = self.status.rebloggedAccountUsername {
|
||||
self.routerPath.navigate(to: .userProfile(accountId: rebloggedAccountId,
|
||||
accountDisplayName: self.status.rebloggedAccountDisplayName,
|
||||
accountUserName: rebloggedAccountUsername))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImageFavourite(isFavourited: $isFavourited)
|
||||
|
@ -156,6 +180,23 @@ struct ImageRowItem: View {
|
|||
}
|
||||
}
|
||||
|
||||
@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 {
|
||||
Image(uiImage: uiImage)
|
||||
|
@ -228,7 +269,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,
|
||||
|
|
|
@ -67,10 +67,21 @@ 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) { isAuthor in
|
||||
if isAuthor {
|
||||
self.routerPath.navigate(to: .userProfile(accountId: self.statusViewModel.account.id,
|
||||
accountDisplayName: self.statusViewModel.account.displayNameWithoutEmojis,
|
||||
accountUserName: self.statusViewModel.account.acct))
|
||||
} else {
|
||||
if let rebloggedAccountId = self.statusViewModel.reblogStatus?.account.id,
|
||||
let rebloggedAccountUsername = self.statusViewModel.reblogStatus?.account.acct {
|
||||
self.routerPath.navigate(to: .userProfile(accountId: rebloggedAccountId,
|
||||
accountDisplayName: self.statusViewModel.reblogStatus?.account.displayNameWithoutEmojis,
|
||||
accountUserName: rebloggedAccountUsername))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -143,10 +154,21 @@ 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) { isAuthor in
|
||||
if isAuthor {
|
||||
self.routerPath.navigate(to: .userProfile(accountId: self.statusViewModel.account.id,
|
||||
accountDisplayName: self.statusViewModel.account.displayNameWithoutEmojis,
|
||||
accountUserName: self.statusViewModel.account.acct))
|
||||
} else {
|
||||
if let rebloggedAccountId = self.statusViewModel.reblogStatus?.account.id,
|
||||
let rebloggedAccountUsername = self.statusViewModel.reblogStatus?.account.acct {
|
||||
self.routerPath.navigate(to: .userProfile(accountId: rebloggedAccountId,
|
||||
accountDisplayName: self.statusViewModel.reblogStatus?.account.displayNameWithoutEmojis,
|
||||
accountUserName: rebloggedAccountUsername))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -84,11 +84,11 @@ struct InteractionRow: View {
|
|||
Spacer()
|
||||
|
||||
Menu {
|
||||
NavigationLink(value: RouteurDestinations.accounts(listType: .reblogged(entityId: statusModel.id))) {
|
||||
NavigationLink(value: RouteurDestinations.accounts(listType: .reblogged(entityId: statusModel.getOrginalStatusId()))) {
|
||||
Label("status.title.reboostedBy", image: "custom.rocket")
|
||||
}
|
||||
|
||||
NavigationLink(value: RouteurDestinations.accounts(listType: .favourited(entityId: statusModel.id))) {
|
||||
NavigationLink(value: RouteurDestinations.accounts(listType: .favourited(entityId: statusModel.getOrginalStatusId()))) {
|
||||
Label("status.title.favouritedBy", systemImage: "star")
|
||||
}
|
||||
|
||||
|
@ -116,7 +116,7 @@ struct InteractionRow: View {
|
|||
Divider()
|
||||
|
||||
Button {
|
||||
self.routerPath.presentedSheet = .report(objectType: .post, objectId: self.statusModel.id)
|
||||
self.routerPath.presentedSheet = .report(objectType: .post, objectId: self.statusModel.getOrginalStatusId())
|
||||
} label: {
|
||||
Label(NSLocalizedString("status.title.report", comment: "Report"), systemImage: "exclamationmark.triangle")
|
||||
}
|
||||
|
@ -144,8 +144,8 @@ struct InteractionRow: View {
|
|||
private func reboost() async {
|
||||
do {
|
||||
let status = self.reblogged
|
||||
? try await self.client.statuses?.unboost(statusId: self.statusModel.id)
|
||||
: try await self.client.statuses?.boost(statusId: self.statusModel.id)
|
||||
? try await self.client.statuses?.unboost(statusId: self.statusModel.getOrginalStatusId())
|
||||
: try await self.client.statuses?.boost(statusId: self.statusModel.getOrginalStatusId())
|
||||
|
||||
if let status {
|
||||
self.reblogsCount = status.reblogsCount == self.reblogsCount
|
||||
|
@ -166,8 +166,8 @@ struct InteractionRow: View {
|
|||
private func favourite() async {
|
||||
do {
|
||||
let status = self.favourited
|
||||
? try await self.client.statuses?.unfavourite(statusId: self.statusModel.id)
|
||||
: try await self.client.statuses?.favourite(statusId: self.statusModel.id)
|
||||
? try await self.client.statuses?.unfavourite(statusId: self.statusModel.getOrginalStatusId())
|
||||
: try await self.client.statuses?.favourite(statusId: self.statusModel.getOrginalStatusId())
|
||||
|
||||
if let status {
|
||||
self.favouritesCount = status.favouritesCount == self.favouritesCount
|
||||
|
@ -188,8 +188,8 @@ struct InteractionRow: View {
|
|||
private func bookmark() async {
|
||||
do {
|
||||
_ = self.bookmarked
|
||||
? try await self.client.statuses?.unbookmark(statusId: self.statusModel.id)
|
||||
: try await self.client.statuses?.bookmark(statusId: self.statusModel.id)
|
||||
? try await self.client.statuses?.unbookmark(statusId: self.statusModel.getOrginalStatusId())
|
||||
: try await self.client.statuses?.bookmark(statusId: self.statusModel.getOrginalStatusId())
|
||||
|
||||
self.bookmarked.toggle()
|
||||
ToastrService.shared.showSuccess(self.bookmarked
|
||||
|
|
|
@ -13,11 +13,13 @@ struct WaterfallGrid<Data, ID, Content>: View where Data: RandomAccessCollection
|
|||
@Binding private var columns: Int
|
||||
@Binding private var hideLoadMore: Bool
|
||||
@Binding private var data: Data
|
||||
@Binding private var refreshId: String
|
||||
|
||||
private let content: (Data.Element) -> Content
|
||||
|
||||
@State private var columnsData: [ColumnData<Data.Element>] = []
|
||||
@State private var processedItems: [Data.Element.ID] = []
|
||||
@State private var shouldRecalculate = false
|
||||
|
||||
private let onLoadMore: () async -> Void
|
||||
private let semaphore = AsyncSemaphore(value: 1)
|
||||
|
@ -46,9 +48,17 @@ struct WaterfallGrid<Data, ID, Content>: View where Data: RandomAccessCollection
|
|||
.onFirstAppear {
|
||||
self.recalculateArrays()
|
||||
}
|
||||
.onChange(of: self.refreshId) { _ in
|
||||
self.shouldRecalculate = true
|
||||
}
|
||||
.onChange(of: self.data) { _ in
|
||||
if self.shouldRecalculate {
|
||||
self.recalculateArrays()
|
||||
self.shouldRecalculate = false
|
||||
} else {
|
||||
self.appendToArrays()
|
||||
}
|
||||
}
|
||||
.onChange(of: self.columns) { _ in
|
||||
self.recalculateArrays()
|
||||
}
|
||||
|
@ -113,25 +123,37 @@ struct WaterfallGrid<Data, ID, Content>: View where Data: RandomAccessCollection
|
|||
}
|
||||
|
||||
extension WaterfallGrid {
|
||||
init(_ data: Binding<Data>, id: KeyPath<Data.Element, ID>, columns: Binding<Int>,
|
||||
hideLoadMore: Binding<Bool>, content: @escaping (Data.Element) -> Content, onLoadMore: @escaping () async -> Void) {
|
||||
init(_ data: Binding<Data>,
|
||||
refreshId: Binding<String>,
|
||||
columns: Binding<Int>,
|
||||
hideLoadMore: Binding<Bool>,
|
||||
content: @escaping (Data.Element) -> Content,
|
||||
onLoadMore: @escaping () async -> Void) {
|
||||
|
||||
self.content = content
|
||||
self.onLoadMore = onLoadMore
|
||||
|
||||
self._data = data
|
||||
self._columns = columns
|
||||
self._hideLoadMore = hideLoadMore
|
||||
self._refreshId = refreshId
|
||||
}
|
||||
}
|
||||
|
||||
extension WaterfallGrid where ID == Data.Element.ID, Data.Element: Identifiable {
|
||||
init(_ data: Binding<Data>, columns: Binding<Int>,
|
||||
hideLoadMore: Binding<Bool>, content: @escaping (Data.Element) -> Content, onLoadMore: @escaping () async -> Void) {
|
||||
init(_ data: Binding<Data>,
|
||||
refreshId: Binding<String>,
|
||||
columns: Binding<Int>,
|
||||
hideLoadMore: Binding<Bool>,
|
||||
content: @escaping (Data.Element) -> Content,
|
||||
onLoadMore: @escaping () async -> Void) {
|
||||
|
||||
self.content = content
|
||||
self.onLoadMore = onLoadMore
|
||||
|
||||
self._data = data
|
||||
self._columns = columns
|
||||
self._hideLoadMore = hideLoadMore
|
||||
self._refreshId = refreshId
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -708,7 +708,7 @@ public struct BaseComposeView: View {
|
|||
}
|
||||
|
||||
private func createStatus() -> Pixelfed.Statuses.Components {
|
||||
return Pixelfed.Statuses.Components(inReplyToId: self.statusViewModel?.id,
|
||||
return Pixelfed.Statuses.Components(inReplyToId: self.statusViewModel?.getOrginalStatusId(),
|
||||
text: self.textModel.text.string,
|
||||
spoilerText: self.isSensitive ? self.spoilerText : String.empty(),
|
||||
mediaIds: self.photosAttachment.getUploadedPhotoIds(),
|
||||
|
|
|
@ -14,45 +14,63 @@ public struct ImageAvatar: View {
|
|||
|
||||
private let displayName: String?
|
||||
private let avatarUrl: URL?
|
||||
private let onTap: () -> Void
|
||||
private let rebloggedAccountDisplayName: String?
|
||||
private let rebloggedAccountAvatar: URL?
|
||||
private let onTap: (Bool) -> Void
|
||||
|
||||
public init(displayName: String?, avatarUrl: URL?, onTap: @escaping () -> Void) {
|
||||
public init(displayName: String?, avatarUrl: URL?, rebloggedAccountDisplayName: String?, rebloggedAccountAvatar: URL?, onTap: @escaping (Bool) -> 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) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 0){
|
||||
HStack(alignment: .center, spacing: 0) {
|
||||
HStack(alignment: .center, spacing: 4) {
|
||||
UserAvatar(accountAvatar: avatarUrl, size: .mini)
|
||||
Text(displayName ?? "")
|
||||
.font(.system(size: 15))
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
.fontWeight(.semibold)
|
||||
.shadow(color: .black, radius: 2)
|
||||
.lineLimit(1)
|
||||
.padding(.trailing, 8)
|
||||
}
|
||||
.padding(8)
|
||||
.font(.footnote)
|
||||
.foregroundColor(.white.opacity(0.8))
|
||||
.background(.black.opacity(0.6))
|
||||
.clipShape(Capsule())
|
||||
.padding(.leading, 8)
|
||||
.padding(.top, 8)
|
||||
.onTapGesture {
|
||||
self.onTap()
|
||||
self.onTap(true)
|
||||
}
|
||||
|
||||
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.6))
|
||||
.clipShape(Capsule())
|
||||
.padding(.leading, 8)
|
||||
.padding(.top, 8)
|
||||
.onTapGesture {
|
||||
self.onTap(false)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.trailing, 58)
|
||||
}
|
||||
}
|
||||
|
||||
|
|