938 lines
34 KiB
Swift
938 lines
34 KiB
Swift
//
|
|
// SolianNotificationWidget.swift
|
|
// Runner
|
|
//
|
|
// Created by LittleSheep on 2026/1/4.
|
|
//
|
|
|
|
import Foundation
|
|
import WidgetKit
|
|
import SwiftUI
|
|
|
|
// MARK: - Notification Widget
|
|
|
|
struct NotificationMeta: Codable {
|
|
let pfp: String?
|
|
let images: [String]?
|
|
let actionUri: String?
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case pfp
|
|
case images
|
|
case actionUri = "action_uri"
|
|
}
|
|
}
|
|
|
|
struct SnNotification: Codable, Identifiable {
|
|
let id: String
|
|
let topic: String
|
|
let title: String
|
|
let subtitle: String
|
|
let content: String
|
|
let meta: NotificationMeta?
|
|
let priority: Int
|
|
let viewedAt: String?
|
|
let accountId: String
|
|
let createdAt: String
|
|
let updatedAt: String
|
|
let deletedAt: String?
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case id
|
|
case topic
|
|
case title
|
|
case subtitle
|
|
case content
|
|
case meta
|
|
case priority
|
|
case viewedAt = "viewed_at"
|
|
case accountId = "account_id"
|
|
case createdAt = "created_at"
|
|
case updatedAt = "updated_at"
|
|
case deletedAt = "deleted_at"
|
|
}
|
|
|
|
var createdDate: Date? {
|
|
let formatter = ISO8601DateFormatter()
|
|
formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
|
|
|
|
if let date = formatter.date(from: createdAt) {
|
|
return date
|
|
}
|
|
|
|
// Fallback for timestamps without fractional seconds
|
|
formatter.formatOptions = [.withInternetDateTime]
|
|
return formatter.date(from: createdAt)
|
|
}
|
|
|
|
var isUnread: Bool {
|
|
return viewedAt == nil
|
|
}
|
|
|
|
func getTopicIcon() -> String {
|
|
switch topic {
|
|
case "post.replies":
|
|
return "arrow.uturn.backward"
|
|
case "wallet.transactions":
|
|
return "wallet.pass"
|
|
case "relationships.friends.request":
|
|
return "person.badge.plus"
|
|
case "invites.chat":
|
|
return "message.badge"
|
|
case "invites.realm":
|
|
return "globe"
|
|
case "auth.login":
|
|
return "arrow.right.square"
|
|
case "posts.new":
|
|
return "doc.badge.plus"
|
|
case "wallet.orders.paid":
|
|
return "baggage"
|
|
case "posts.reactions.new":
|
|
return "face.smiling"
|
|
default:
|
|
return "bell"
|
|
}
|
|
}
|
|
}
|
|
|
|
struct NotificationEntry: TimelineEntry {
|
|
let date: Date
|
|
let notifications: [SnNotification]?
|
|
let unreadCount: Int
|
|
let error: String?
|
|
let isLoading: Bool
|
|
|
|
static func placeholder() -> NotificationEntry {
|
|
NotificationEntry(date: Date(), notifications: nil, unreadCount: 0, error: nil, isLoading: true)
|
|
}
|
|
}
|
|
|
|
class NotificationService {
|
|
private let networkService = WidgetNetworkService()
|
|
|
|
private lazy var session: URLSession = {
|
|
let configuration = URLSessionConfiguration.ephemeral
|
|
configuration.timeoutIntervalForRequest = 10.0
|
|
configuration.timeoutIntervalForResource = 10.0
|
|
configuration.waitsForConnectivity = false
|
|
return URLSession(configuration: configuration)
|
|
}()
|
|
|
|
func fetchRecentNotifications(take: Int = 5) async throws -> [SnNotification] {
|
|
if take == 0 {
|
|
return []
|
|
}
|
|
|
|
guard let token = networkService.token else {
|
|
throw RemoteError.missingCredentials
|
|
}
|
|
|
|
let baseURL = networkService.baseURL
|
|
guard let url = URL(string: "\(baseURL)/ring/notifications?unmark=true&take=\(take)") else {
|
|
throw RemoteError.invalidURL
|
|
}
|
|
|
|
var request = URLRequest(url: url)
|
|
request.httpMethod = "GET"
|
|
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
|
request.timeoutInterval = 10.0
|
|
|
|
let (data, response) = try await session.data(for: request)
|
|
|
|
guard let httpResponse = response as? HTTPURLResponse else {
|
|
throw RemoteError.invalidResponse
|
|
}
|
|
|
|
switch httpResponse.statusCode {
|
|
case 200...299:
|
|
let decoder = JSONDecoder()
|
|
let notifications = try decoder.decode([SnNotification].self, from: data)
|
|
return notifications
|
|
case 404:
|
|
return []
|
|
default:
|
|
throw RemoteError.httpError(httpResponse.statusCode)
|
|
}
|
|
}
|
|
|
|
func fetchUnreadCount() async throws -> Int {
|
|
guard let token = networkService.token else {
|
|
throw RemoteError.missingCredentials
|
|
}
|
|
|
|
let baseURL = networkService.baseURL
|
|
guard let url = URL(string: "\(baseURL)/ring/notifications/count") else {
|
|
throw RemoteError.invalidURL
|
|
}
|
|
|
|
var request = URLRequest(url: url)
|
|
request.httpMethod = "GET"
|
|
request.setValue("AtField \(token)", forHTTPHeaderField: "Authorization")
|
|
request.setValue("application/json", forHTTPHeaderField: "Accept")
|
|
request.timeoutInterval = 10.0
|
|
|
|
let (data, response) = try await session.data(for: request)
|
|
|
|
guard let httpResponse = response as? HTTPURLResponse else {
|
|
throw RemoteError.invalidResponse
|
|
}
|
|
|
|
switch httpResponse.statusCode {
|
|
case 200...299:
|
|
if let count = try? JSONSerialization.jsonObject(with: data) as? Int {
|
|
return count
|
|
} else if let count = try? JSONSerialization.jsonObject(with: data) as? Double {
|
|
return Int(count)
|
|
}
|
|
return 0
|
|
case 404:
|
|
return 0
|
|
default:
|
|
throw RemoteError.httpError(httpResponse.statusCode)
|
|
}
|
|
}
|
|
}
|
|
|
|
struct NotificationProvider: TimelineProvider {
|
|
private let notificationService = NotificationService()
|
|
|
|
func placeholder(in context: Context) -> NotificationEntry {
|
|
NotificationEntry.placeholder()
|
|
}
|
|
|
|
func getSnapshot(in context: Context, completion: @escaping (NotificationEntry) -> ()) {
|
|
Task {
|
|
print("[WidgetKit] [NotificationProvider] Getting snapshot...")
|
|
async let notifications = try? await notificationService.fetchRecentNotifications(take: 5)
|
|
async let unreadCount = try? await notificationService.fetchUnreadCount()
|
|
|
|
let notifs = try? await notifications
|
|
let unread = (try? await unreadCount) ?? 0
|
|
|
|
print("[WidgetKit] [NotificationProvider] Snapshot - Notifications: \(notifs?.count ?? 0), Unread: \(unread)")
|
|
|
|
let entry = NotificationEntry(date: Date(), notifications: notifs, unreadCount: unread, error: nil, isLoading: false)
|
|
completion(entry)
|
|
}
|
|
}
|
|
|
|
func getTimeline(in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
|
|
Task {
|
|
let currentDate = Date()
|
|
print("[WidgetKit] [NotificationProvider] Getting timeline at \(currentDate)...")
|
|
|
|
do {
|
|
let takeLimit: Int
|
|
switch context.family {
|
|
case .systemSmall:
|
|
takeLimit = 0
|
|
case .systemMedium:
|
|
takeLimit = 1
|
|
case .systemLarge:
|
|
takeLimit = 3
|
|
default:
|
|
takeLimit = 0
|
|
}
|
|
|
|
async let notifications = try await notificationService.fetchRecentNotifications(take: takeLimit)
|
|
async let unreadCount = try await notificationService.fetchUnreadCount()
|
|
|
|
let notifs = try await notifications
|
|
let unread = try await unreadCount
|
|
|
|
print("[WidgetKit] [NotificationProvider] Timeline - Notifications: \(notifs.count), Unread: \(unread)")
|
|
|
|
let entry = NotificationEntry(date: currentDate, notifications: notifs, unreadCount: unread, error: nil, isLoading: false)
|
|
|
|
let nextUpdate = Calendar.current.date(byAdding: .minute, value: 15, to: currentDate)!
|
|
print("[WidgetKit] [NotificationProvider] Next update at: \(nextUpdate)")
|
|
let timeline = Timeline(entries: [entry], policy: .after(nextUpdate))
|
|
completion(timeline)
|
|
} catch {
|
|
print("[WidgetKit] [NotificationProvider] Error in getTimeline: \(error.localizedDescription)")
|
|
let entry = NotificationEntry(date: currentDate, notifications: nil, unreadCount: 0, error: error.localizedDescription, isLoading: false)
|
|
let nextUpdate = Calendar.current.date(byAdding: .minute, value: 5, to: currentDate)!
|
|
let timeline = Timeline(entries: [entry], policy: .after(nextUpdate))
|
|
completion(timeline)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct NotificationWidgetEntryView: View {
|
|
var entry: NotificationProvider.Entry
|
|
@Environment(\.widgetFamily) var family
|
|
|
|
var body: some View {
|
|
if let notifications = entry.notifications, !notifications.isEmpty {
|
|
HasNotificationsView(notifications: notifications, unreadCount: entry.unreadCount)
|
|
} else if entry.isLoading {
|
|
LoadingView()
|
|
} else if let error = entry.error {
|
|
ErrorView(error: error)
|
|
} else {
|
|
EmptyView()
|
|
}
|
|
}
|
|
|
|
private var isCompact: Bool {
|
|
family == .systemSmall || isAccessory
|
|
}
|
|
|
|
private var isAccessory: Bool {
|
|
if #available(iOS 16.0, *) {
|
|
if case .accessoryRectangular = family {
|
|
return true
|
|
}
|
|
if case .accessoryCircular = family {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
private var isCircular: Bool {
|
|
if #available(iOS 16.0, *) {
|
|
if case .accessoryCircular = family {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func HasNotificationsView(notifications: [SnNotification], unreadCount: Int) -> some View {
|
|
Link(destination: URL(string: "solian://notifications")!) {
|
|
if isCompact {
|
|
if isAccessory {
|
|
if isCircular {
|
|
ZStack {
|
|
Image(systemName: "bell.fill")
|
|
.font(.system(size: 20))
|
|
.foregroundColor(unreadCount > 0 ? .orange : .gray)
|
|
|
|
if unreadCount > 0 {
|
|
Text("\(min(unreadCount, 99))")
|
|
.font(.system(size: 10, weight: .bold))
|
|
.foregroundColor(.white)
|
|
.padding(4)
|
|
.background(Circle().fill(Color.blue))
|
|
.offset(x: 12, y: -12)
|
|
}
|
|
}
|
|
} else {
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
HStack(spacing: 4) {
|
|
Image(systemName: "bell.fill")
|
|
.font(.caption2)
|
|
.foregroundColor(.orange)
|
|
.padding(.leading, 1.5)
|
|
|
|
Text(NSLocalizedString("notifications", comment: "Notifications"))
|
|
.font(.caption2)
|
|
.fontWeight(.bold)
|
|
|
|
Spacer()
|
|
}
|
|
|
|
if unreadCount > 0 {
|
|
HStack(spacing: 4) {
|
|
Text("\(unreadCount)")
|
|
.font(.caption2)
|
|
.fontWeight(.bold)
|
|
.padding(.horizontal, 6)
|
|
.background(
|
|
Capsule()
|
|
.fill(Color.blue.opacity(0.5))
|
|
)
|
|
|
|
Text(NSLocalizedString("unread", comment: "unread"))
|
|
.font(.caption2)
|
|
}
|
|
}
|
|
|
|
Text("on the Solar Network")
|
|
.font(.caption2)
|
|
.foregroundColor(.secondary)
|
|
.padding(.horizontal, 1.5)
|
|
}
|
|
}
|
|
} else {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
HStack(spacing: 6) {
|
|
Image(systemName: "bell.fill")
|
|
.font(.subheadline)
|
|
.foregroundColor(.orange)
|
|
|
|
Text(NSLocalizedString("notifications", comment: "Notifications"))
|
|
.font(.headline)
|
|
.fontWeight(.bold)
|
|
|
|
Spacer()
|
|
}.padding(.bottom, 8)
|
|
|
|
Spacer(minLength: 2)
|
|
|
|
if unreadCount > 0 {
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text("\(unreadCount)")
|
|
.font(.system(.largeTitle, design: .rounded))
|
|
.fontWeight(.bold)
|
|
|
|
|
|
}
|
|
}
|
|
|
|
Spacer(minLength: 2)
|
|
|
|
if unreadCount > 0 {
|
|
Text(NSLocalizedString("unreadNotifications", comment: "unread notifications"))
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
.lineLimit(2)
|
|
.multilineTextAlignment(.leading)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
} else {
|
|
Text(NSLocalizedString("noUnreadNotifications", comment: "no unread notifications"))
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
.padding(12)
|
|
}
|
|
} else {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
HStack(spacing: 6) {
|
|
Image(systemName: "bell.fill")
|
|
.font(.subheadline)
|
|
.foregroundColor(.orange)
|
|
|
|
Text(NSLocalizedString("notifications", comment: "Notifications"))
|
|
.font(.headline)
|
|
.fontWeight(.bold)
|
|
|
|
Spacer()
|
|
|
|
if unreadCount > 0 {
|
|
Text("\(unreadCount)")
|
|
.font(.caption)
|
|
.fontWeight(.bold)
|
|
.foregroundColor(.white)
|
|
.padding(.horizontal, 6)
|
|
.padding(.vertical, 2)
|
|
.background(.blue)
|
|
.clipShape(Capsule())
|
|
}
|
|
}.padding(.bottom, 8)
|
|
|
|
let displayCount = family == .systemMedium ? 1 : 5
|
|
let displayNotifications = Array(notifications.prefix(displayCount))
|
|
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
ForEach(displayNotifications) { notification in
|
|
NotificationItemView(notification: notification, compact: false)
|
|
}
|
|
}
|
|
|
|
if family == .systemMedium {
|
|
Spacer()
|
|
} else {
|
|
Spacer()
|
|
Text(NSLocalizedString("tapToViewAll", comment: "Tap to view all notifications"))
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
}
|
|
.padding(12)
|
|
}
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func NotificationItemView(notification: SnNotification, compact: Bool) -> some View {
|
|
HStack(alignment: .top, spacing: compact ? 6 : 12) {
|
|
if compact {
|
|
Image(systemName: notification.getTopicIcon())
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
} else {
|
|
ZStack {
|
|
Circle()
|
|
.fill(Color.gray.opacity(0.2))
|
|
.frame(width: 32, height: 32)
|
|
|
|
Image(systemName: notification.getTopicIcon())
|
|
.font(.caption)
|
|
.foregroundColor(.primary)
|
|
}
|
|
}
|
|
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(notification.title)
|
|
.font(compact ? .caption : .subheadline)
|
|
.fontWeight(notification.isUnread ? .semibold : .regular)
|
|
.lineLimit(1)
|
|
|
|
if !compact && !notification.subtitle.isEmpty {
|
|
Text(notification.subtitle)
|
|
.font(.caption)
|
|
.lineLimit(1)
|
|
}
|
|
|
|
if !compact && !notification.content.isEmpty {
|
|
Text(notification.content)
|
|
.font(.caption2)
|
|
.lineLimit(2)
|
|
.multilineTextAlignment(.leading)
|
|
}
|
|
|
|
if let createdDate = notification.createdDate {
|
|
Text(formatRelativeTime(createdDate))
|
|
.font(.caption2)
|
|
.foregroundColor(.secondary)
|
|
.padding(.top, 2)
|
|
}
|
|
}
|
|
|
|
Spacer()
|
|
|
|
if notification.isUnread {
|
|
Circle()
|
|
.fill(Color.blue)
|
|
.frame(width: compact ? 6 : 8, height: compact ? 6 : 8)
|
|
.padding(.trailing, 6)
|
|
.padding(.top, 8)
|
|
}
|
|
}
|
|
.padding(.vertical, compact ? 2 : 4)
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func NotificationCompactItem(notification: SnNotification) -> some View {
|
|
HStack(spacing: 4) {
|
|
Image(systemName: notification.getTopicIcon())
|
|
.font(.caption2)
|
|
.foregroundColor(.secondary)
|
|
|
|
Text(notification.title)
|
|
.font(.caption2)
|
|
.lineLimit(1)
|
|
.fontWeight(notification.isUnread ? .semibold : .regular)
|
|
|
|
Spacer()
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func EmptyView() -> some View {
|
|
Link(destination: URL(string: "solian://notifications")!) {
|
|
if isCircular {
|
|
ZStack {
|
|
Image(systemName: "bell")
|
|
.font(.system(size: 20))
|
|
.foregroundColor(.gray)
|
|
}
|
|
} else {
|
|
VStack(alignment: .leading, spacing: isAccessory ? 4 : 8) {
|
|
HStack(spacing: 6) {
|
|
Image(systemName: "bell")
|
|
.font(isAccessory ? .caption : .title3)
|
|
.foregroundColor(.secondary)
|
|
|
|
Text(NSLocalizedString("notifications", comment: "Notifications"))
|
|
.font(isAccessory ? .caption2 : .headline)
|
|
.fontWeight(.bold)
|
|
|
|
Spacer()
|
|
}
|
|
|
|
if !isAccessory {
|
|
Text(NSLocalizedString("noNotifications", comment: "No notifications yet"))
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
|
|
Spacer()
|
|
}
|
|
}
|
|
.padding(isAccessory ? 4 : 12)
|
|
}
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func LoadingView() -> some View {
|
|
if isCircular {
|
|
ProgressView()
|
|
.scaleEffect(0.8)
|
|
} else {
|
|
VStack(alignment: .leading, spacing: isAccessory ? 4 : 8) {
|
|
HStack(spacing: 6) {
|
|
ProgressView()
|
|
.scaleEffect(isAccessory ? 0.6 : 0.8)
|
|
Text(NSLocalizedString("loading", comment: "Loading..."))
|
|
.font(isAccessory ? .caption2 : .caption)
|
|
.foregroundColor(.secondary)
|
|
Spacer()
|
|
}
|
|
|
|
if !isAccessory {
|
|
Spacer()
|
|
}
|
|
}
|
|
.padding(isAccessory ? 4 : 12)
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func ErrorView(error: String) -> some View {
|
|
Link(destination: URL(string: "solian://notifications")!) {
|
|
if isCircular {
|
|
ZStack {
|
|
Image(systemName: "exclamationmark.triangle")
|
|
.font(.system(size: 20))
|
|
.foregroundColor(.red)
|
|
}
|
|
} else {
|
|
VStack(alignment: .leading, spacing: isAccessory ? 4 : 8) {
|
|
HStack(spacing: 6) {
|
|
Image(systemName: "exclamationmark.triangle")
|
|
.foregroundColor(.secondary)
|
|
.font(isAccessory ? .caption : .title3)
|
|
|
|
Text(NSLocalizedString("error", comment: "Error"))
|
|
.font(isAccessory ? .caption2 : .headline)
|
|
Spacer()
|
|
}
|
|
|
|
if !isAccessory {
|
|
Text(NSLocalizedString("openAppToRefresh", comment: "Open app to refresh"))
|
|
.font(.caption)
|
|
.foregroundColor(.secondary)
|
|
|
|
Text(error)
|
|
.font(.footnote)
|
|
.foregroundStyle(.secondary)
|
|
.lineLimit(nil)
|
|
.multilineTextAlignment(.leading)
|
|
|
|
Spacer()
|
|
}
|
|
}
|
|
.padding(isAccessory ? 4 : 12)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func formatRelativeTime(_ date: Date) -> String {
|
|
let now = Date()
|
|
let interval = now.timeIntervalSince(date)
|
|
|
|
if interval < 60 {
|
|
return NSLocalizedString("justNow", comment: "Just now")
|
|
} else if interval < 3600 {
|
|
let minutes = Int(interval / 60)
|
|
return String(format: NSLocalizedString("minutesAgo", comment: "%d min ago"), minutes)
|
|
} else if interval < 86400 {
|
|
let hours = Int(interval / 3600)
|
|
return String(format: NSLocalizedString("hoursAgo", comment: "%d hr ago"), hours)
|
|
} else {
|
|
let days = Int(interval / 86400)
|
|
return String(format: NSLocalizedString("daysAgo", comment: "%d d ago"), days)
|
|
}
|
|
}
|
|
}
|
|
|
|
struct NotificationWidgetRootView: View {
|
|
var entry: NotificationProvider.Entry
|
|
@Environment(\.colorScheme) var colorScheme
|
|
|
|
var body: some View {
|
|
if #available(iOS 17.0, *) {
|
|
ZStack {
|
|
NotificationWidgetEntryView(entry: entry)
|
|
|
|
if let notifications = entry.notifications, !notifications.isEmpty {
|
|
GeometryReader { geometry in
|
|
Image(colorScheme == .dark ? "CloudyLambDark" : "CloudyLamb")
|
|
.resizable()
|
|
.aspectRatio(contentMode: .fit)
|
|
.frame(
|
|
width: geometry.size.width * 0.9,
|
|
height: geometry.size.width * 0.9
|
|
)
|
|
.opacity(0.12)
|
|
.mask(
|
|
LinearGradient(
|
|
gradient: Gradient(colors: [
|
|
Color.white,
|
|
Color.white,
|
|
Color.clear
|
|
]),
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing
|
|
)
|
|
)
|
|
.position(
|
|
x: geometry.size.width * 0.85,
|
|
y: 20
|
|
)
|
|
}
|
|
.allowsHitTesting(false)
|
|
}
|
|
}
|
|
.containerBackground(.fill.tertiary, for: .widget)
|
|
.padding(.vertical, 8)
|
|
} else {
|
|
NotificationWidgetEntryView(entry: entry)
|
|
.padding()
|
|
.background()
|
|
}
|
|
}
|
|
}
|
|
|
|
struct SolianNotificationWidget: Widget {
|
|
let kind: String = "SolianNotificationWidget"
|
|
|
|
var body: some WidgetConfiguration {
|
|
StaticConfiguration(kind: kind, provider: NotificationProvider()) { entry in
|
|
NotificationWidgetRootView(entry: entry)
|
|
}
|
|
.configurationDisplayName("Notifications")
|
|
.description("View your recent notifications")
|
|
.supportedFamilies(supportedFamilies)
|
|
}
|
|
|
|
private var supportedFamilies: [WidgetFamily] {
|
|
#if os(iOS)
|
|
return [.systemSmall, .systemMedium, .systemLarge, .accessoryRectangular, .accessoryCircular]
|
|
#else
|
|
return [.systemSmall, .systemMedium, .systemLarge]
|
|
#endif
|
|
}
|
|
}
|
|
|
|
#if os(iOS)
|
|
#Preview(as: .accessoryRectangular) {
|
|
SolianNotificationWidget()
|
|
} timeline: {
|
|
NotificationEntry(
|
|
date: .now,
|
|
notifications: [
|
|
SnNotification(
|
|
id: "1",
|
|
topic: "post.replies",
|
|
title: "New reply to your post",
|
|
subtitle: "Someone replied to your message",
|
|
content: "This is notification content",
|
|
meta: nil,
|
|
priority: 0,
|
|
viewedAt: nil,
|
|
accountId: "acc-1",
|
|
createdAt: ISO8601DateFormatter().string(from: Date()),
|
|
updatedAt: ISO8601DateFormatter().string(from: Date()),
|
|
deletedAt: nil
|
|
),
|
|
SnNotification(
|
|
id: "2",
|
|
topic: "relationships.friends.request",
|
|
title: "New friend request",
|
|
subtitle: "You have a pending friend request",
|
|
content: "Someone wants to be your friend",
|
|
meta: nil,
|
|
priority: 0,
|
|
viewedAt: ISO8601DateFormatter().string(from: Date()),
|
|
accountId: "acc-1",
|
|
createdAt: ISO8601DateFormatter().string(from: Date().addingTimeInterval(-3600)),
|
|
updatedAt: ISO8601DateFormatter().string(from: Date()),
|
|
deletedAt: nil
|
|
)
|
|
],
|
|
unreadCount: 1,
|
|
error: nil,
|
|
isLoading: false
|
|
)
|
|
}
|
|
#endif
|
|
|
|
#Preview(as: .systemSmall) {
|
|
SolianNotificationWidget()
|
|
} timeline: {
|
|
NotificationEntry(
|
|
date: .now,
|
|
notifications: [
|
|
SnNotification(
|
|
id: "1",
|
|
topic: "post.replies",
|
|
title: "New reply to your post",
|
|
subtitle: "Someone replied to your message",
|
|
content: "This is notification content",
|
|
meta: nil,
|
|
priority: 0,
|
|
viewedAt: nil,
|
|
accountId: "acc-1",
|
|
createdAt: ISO8601DateFormatter().string(from: Date()),
|
|
updatedAt: ISO8601DateFormatter().string(from: Date()),
|
|
deletedAt: nil
|
|
),
|
|
SnNotification(
|
|
id: "2",
|
|
topic: "relationships.friends.request",
|
|
title: "New friend request",
|
|
subtitle: "You have a pending friend request",
|
|
content: "Someone wants to be your friend",
|
|
meta: nil,
|
|
priority: 0,
|
|
viewedAt: ISO8601DateFormatter().string(from: Date()),
|
|
accountId: "acc-1",
|
|
createdAt: ISO8601DateFormatter().string(from: Date().addingTimeInterval(-3600)),
|
|
updatedAt: ISO8601DateFormatter().string(from: Date()),
|
|
deletedAt: nil
|
|
)
|
|
],
|
|
unreadCount: 1,
|
|
error: nil,
|
|
isLoading: false
|
|
)
|
|
}
|
|
|
|
#Preview(as: .systemMedium) {
|
|
SolianNotificationWidget()
|
|
} timeline: {
|
|
NotificationEntry(
|
|
date: .now,
|
|
notifications: [
|
|
SnNotification(
|
|
id: "1",
|
|
topic: "post.replies",
|
|
title: "New reply to your post",
|
|
subtitle: "Someone replied to your message",
|
|
content: "This is notification content",
|
|
meta: nil,
|
|
priority: 0,
|
|
viewedAt: nil,
|
|
accountId: "acc-1",
|
|
createdAt: ISO8601DateFormatter().string(from: Date()),
|
|
updatedAt: ISO8601DateFormatter().string(from: Date()),
|
|
deletedAt: nil
|
|
),
|
|
SnNotification(
|
|
id: "2",
|
|
topic: "relationships.friends.request",
|
|
title: "New friend request",
|
|
subtitle: "You have a pending friend request",
|
|
content: "Someone wants to be your friend",
|
|
meta: nil,
|
|
priority: 0,
|
|
viewedAt: nil,
|
|
accountId: "acc-1",
|
|
createdAt: ISO8601DateFormatter().string(from: Date().addingTimeInterval(-3600)),
|
|
updatedAt: ISO8601DateFormatter().string(from: Date()),
|
|
deletedAt: nil
|
|
),
|
|
SnNotification(
|
|
id: "3",
|
|
topic: "invites.chat",
|
|
title: "New chat invite",
|
|
subtitle: "You've been invited to a chat",
|
|
content: "Join the conversation",
|
|
meta: nil,
|
|
priority: 0,
|
|
viewedAt: ISO8601DateFormatter().string(from: Date().addingTimeInterval(-86400)),
|
|
accountId: "acc-1",
|
|
createdAt: ISO8601DateFormatter().string(from: Date().addingTimeInterval(-86400)),
|
|
updatedAt: ISO8601DateFormatter().string(from: Date()),
|
|
deletedAt: nil
|
|
)
|
|
],
|
|
unreadCount: 2,
|
|
error: nil,
|
|
isLoading: false
|
|
)
|
|
}
|
|
|
|
#if os(iOS)
|
|
#Preview(as: .systemLarge) {
|
|
SolianNotificationWidget()
|
|
} timeline: {
|
|
NotificationEntry(
|
|
date: .now,
|
|
notifications: [
|
|
SnNotification(
|
|
id: "1",
|
|
topic: "post.replies",
|
|
title: "New reply",
|
|
subtitle: "Someone replied",
|
|
content: "Content",
|
|
meta: nil,
|
|
priority: 0,
|
|
viewedAt: nil,
|
|
accountId: "acc-1",
|
|
createdAt: ISO8601DateFormatter().string(from: Date()),
|
|
updatedAt: ISO8601DateFormatter().string(from: Date()),
|
|
deletedAt: nil
|
|
),
|
|
SnNotification(
|
|
id: "2",
|
|
topic: "relationships.friends.request",
|
|
title: "New friend request",
|
|
subtitle: "You have a pending friend request",
|
|
content: "Someone wants to be your friend",
|
|
meta: nil,
|
|
priority: 0,
|
|
viewedAt: nil,
|
|
accountId: "acc-1",
|
|
createdAt: ISO8601DateFormatter().string(from: Date().addingTimeInterval(-3600)),
|
|
updatedAt: ISO8601DateFormatter().string(from: Date()),
|
|
deletedAt: nil
|
|
),
|
|
SnNotification(
|
|
id: "3",
|
|
topic: "invites.chat",
|
|
title: "New chat invite",
|
|
subtitle: "You've been invited to a chat",
|
|
content: "Join the conversation",
|
|
meta: nil,
|
|
priority: 0,
|
|
viewedAt: ISO8601DateFormatter().string(from: Date().addingTimeInterval(-86400)),
|
|
accountId: "acc-1",
|
|
createdAt: ISO8601DateFormatter().string(from: Date().addingTimeInterval(-86400)),
|
|
updatedAt: ISO8601DateFormatter().string(from: Date()),
|
|
deletedAt: nil
|
|
)
|
|
],
|
|
unreadCount: 3,
|
|
error: nil,
|
|
isLoading: false
|
|
)
|
|
}
|
|
|
|
#Preview(as: .accessoryCircular) {
|
|
SolianNotificationWidget()
|
|
} timeline: {
|
|
NotificationEntry(
|
|
date: .now,
|
|
notifications: [
|
|
SnNotification(
|
|
id: "1",
|
|
topic: "post.replies",
|
|
title: "New reply",
|
|
subtitle: "Someone replied",
|
|
content: "Content",
|
|
meta: nil,
|
|
priority: 0,
|
|
viewedAt: nil,
|
|
accountId: "acc-1",
|
|
createdAt: ISO8601DateFormatter().string(from: Date()),
|
|
updatedAt: ISO8601DateFormatter().string(from: Date()),
|
|
deletedAt: nil
|
|
)
|
|
],
|
|
unreadCount: 5,
|
|
error: nil,
|
|
isLoading: false
|
|
)
|
|
}
|
|
#endif
|