mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
904 lines
34 KiB
Swift
904 lines
34 KiB
Swift
#if arch(arm64) || arch(x86_64)
|
|
|
|
import UIKit
|
|
import NotificationCenter
|
|
import BuildConfig
|
|
import WidgetItems
|
|
import AppLockState
|
|
import SwiftUI
|
|
import WidgetKit
|
|
import Intents
|
|
|
|
import OpenSSLEncryptionProvider
|
|
import SwiftSignalKit
|
|
import Postbox
|
|
import TelegramCore
|
|
import OpenSSLEncryptionProvider
|
|
import WidgetItemsUtils
|
|
|
|
import GeneratedSources
|
|
|
|
struct ParsedPeer {
|
|
var accountId: Int64
|
|
var accountPeerId: Int64
|
|
var peer: WidgetDataPeer
|
|
}
|
|
|
|
struct ParsedPeers {
|
|
var peers: [ParsedPeer]
|
|
var updateTimestamp: Int32
|
|
}
|
|
|
|
private extension ParsedPeers {
|
|
init(accountId: Int64, peers: WidgetDataPeers) {
|
|
self.init(peers: peers.peers.map { peer -> ParsedPeer in
|
|
return ParsedPeer(
|
|
accountId: accountId,
|
|
accountPeerId: peers.accountPeerId,
|
|
peer: peer
|
|
)
|
|
}, updateTimestamp: peers.updateTimestamp)
|
|
}
|
|
}
|
|
|
|
private var installedSharedLogger = false
|
|
|
|
private func setupSharedLogger(rootPath: String, path: String) {
|
|
if !installedSharedLogger {
|
|
installedSharedLogger = true
|
|
Logger.setSharedLogger(Logger(rootPath: rootPath, basePath: path))
|
|
}
|
|
}
|
|
|
|
private let accountAuxiliaryMethods = AccountAuxiliaryMethods(fetchResource: { account, resource, ranges, _ in
|
|
return nil
|
|
}, fetchResourceMediaReferenceHash: { resource in
|
|
return .single(nil)
|
|
}, prepareSecretThumbnailData: { _ in
|
|
return nil
|
|
})
|
|
|
|
private func rootPathForBasePath(_ appGroupPath: String) -> String {
|
|
return appGroupPath + "/telegram-data"
|
|
}
|
|
|
|
@available(iOS 14.0, *)
|
|
private func getCommonTimeline(friends: [Friend]?, in context: TimelineProviderContext, completion: @escaping (Timeline<SimpleEntry>) -> ()) {
|
|
if context.isPreview {
|
|
completion(Timeline(entries: [SimpleEntry(date: Date(), contents: .preview)], policy: .atEnd))
|
|
return
|
|
}
|
|
|
|
let currentDate = Date()
|
|
let entryDate = Calendar.current.date(byAdding: .hour, value: 0, to: currentDate)!
|
|
|
|
guard let appBundleIdentifier = Bundle.main.bundleIdentifier, let lastDotRange = appBundleIdentifier.range(of: ".", options: [.backwards]) else {
|
|
completion(Timeline(entries: [SimpleEntry(date: entryDate, contents: .recent)], policy: .atEnd))
|
|
return
|
|
}
|
|
|
|
let baseAppBundleId = String(appBundleIdentifier[..<lastDotRange.lowerBound])
|
|
|
|
let appGroupName = "group.\(baseAppBundleId)"
|
|
let maybeAppGroupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupName)
|
|
|
|
guard let appGroupUrl = maybeAppGroupUrl else {
|
|
completion(Timeline(entries: [SimpleEntry(date: entryDate, contents: .recent)], policy: .atEnd))
|
|
return
|
|
}
|
|
|
|
let rootPath = rootPathForBasePath(appGroupUrl.path)
|
|
|
|
TempBox.initializeShared(basePath: rootPath, processType: "widget", launchSpecificId: Int64.random(in: Int64.min ... Int64.max))
|
|
|
|
let logsPath = rootPath + "/logs/widget-logs"
|
|
let _ = try? FileManager.default.createDirectory(atPath: logsPath, withIntermediateDirectories: true, attributes: nil)
|
|
|
|
setupSharedLogger(rootPath: logsPath, path: logsPath)
|
|
|
|
initializeAccountManagement()
|
|
|
|
let deviceSpecificEncryptionParameters = BuildConfig.deviceSpecificEncryptionParameters(rootPath, baseAppBundleId: baseAppBundleId)
|
|
let encryptionParameters = ValueBoxEncryptionParameters(forceEncryptionIfNoSet: false, key: ValueBoxEncryptionParameters.Key(data: deviceSpecificEncryptionParameters.key)!, salt: ValueBoxEncryptionParameters.Salt(data: deviceSpecificEncryptionParameters.salt)!)
|
|
|
|
var itemsByAccount: [Int64: [(Int64, Friend)]] = [:]
|
|
var itemOrder: [(Int64, Int64)] = []
|
|
if let friends = friends {
|
|
for item in friends {
|
|
guard let identifier = item.identifier else {
|
|
continue
|
|
}
|
|
guard let index = identifier.firstIndex(of: ":") else {
|
|
continue
|
|
}
|
|
guard let accountId = Int64(identifier[identifier.startIndex ..< index]) else {
|
|
continue
|
|
}
|
|
guard let peerId = Int64(identifier[identifier.index(after: index)...]) else {
|
|
continue
|
|
}
|
|
if itemsByAccount[accountId] == nil {
|
|
itemsByAccount[accountId] = []
|
|
}
|
|
itemsByAccount[accountId]?.append((peerId, item))
|
|
itemOrder.append((accountId, peerId))
|
|
}
|
|
}
|
|
|
|
var friendsByAccount: [Signal<[ParsedPeer], NoError>] = []
|
|
for (accountId, items) in itemsByAccount {
|
|
friendsByAccount.append(accountTransaction(rootPath: rootPath, id: AccountRecordId(rawValue: accountId), encryptionParameters: encryptionParameters, isReadOnly: true, useCopy: false, transaction: { postbox, transaction -> [ParsedPeer] in
|
|
guard let state = transaction.getState() as? AuthorizedAccountState else {
|
|
return []
|
|
}
|
|
|
|
var result: [ParsedPeer] = []
|
|
|
|
for (peerId, _) in items {
|
|
guard let peer = transaction.getPeer(PeerId(peerId)) else {
|
|
continue
|
|
}
|
|
|
|
var name: String = ""
|
|
var lastName: String?
|
|
|
|
if let user = peer as? TelegramUser {
|
|
if let firstName = user.firstName {
|
|
name = firstName
|
|
lastName = user.lastName
|
|
} else if let lastName = user.lastName {
|
|
name = lastName
|
|
} else if let phone = user.phone, !phone.isEmpty {
|
|
name = phone
|
|
}
|
|
} else {
|
|
name = peer.debugDisplayTitle
|
|
}
|
|
|
|
var badge: WidgetDataPeer.Badge?
|
|
|
|
if let readState = transaction.getCombinedPeerReadState(peer.id), readState.count > 0 {
|
|
var isMuted = false
|
|
if let notificationSettings = transaction.getPeerNotificationSettings(id: peer.id) as? TelegramPeerNotificationSettings {
|
|
isMuted = notificationSettings.isRemovedFromTotalUnreadCount(default: false)
|
|
}
|
|
badge = WidgetDataPeer.Badge(
|
|
count: Int(readState.count),
|
|
isMuted: isMuted
|
|
)
|
|
}
|
|
|
|
var mappedMessage: WidgetDataPeer.Message?
|
|
if let index = transaction.getTopPeerMessageIndex(peerId: peer.id) {
|
|
if let message = transaction.getMessage(index.id) {
|
|
mappedMessage = WidgetDataPeer.Message(accountPeerId: state.peerId, message: EngineMessage(message))
|
|
}
|
|
}
|
|
|
|
let widgetPeer = WidgetDataPeer(id: peer.id.toInt64(), name: name, lastName: lastName, letters: peer.displayLetters, avatarPath: smallestImageRepresentation(peer.profileImageRepresentations).flatMap { representation in
|
|
return postbox.mediaBox.resourcePath(representation.resource)
|
|
}, badge: badge, message: mappedMessage)
|
|
|
|
result.append(ParsedPeer(accountId: accountId, accountPeerId: state.peerId.toInt64(), peer: widgetPeer))
|
|
}
|
|
|
|
return result
|
|
})
|
|
|> `catch` { _ -> Signal<[ParsedPeer], NoError> in
|
|
return .single([])
|
|
})
|
|
}
|
|
|
|
let _ = combineLatest(friendsByAccount).start(next: { allPeers in
|
|
var orderedPeers: [ParsedPeer] = []
|
|
|
|
outer: for (accountId, peerId) in itemOrder {
|
|
for peerSet in allPeers {
|
|
for peer in peerSet {
|
|
if peer.accountId == accountId && peer.peer.id == peerId {
|
|
orderedPeers.append(peer)
|
|
continue outer
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let result = ParsedPeers(peers: orderedPeers, updateTimestamp: Int32(Date().timeIntervalSince1970))
|
|
completion(Timeline(entries: [SimpleEntry(date: entryDate, contents: .peers(result))], policy: .atEnd))
|
|
})
|
|
}
|
|
|
|
@available(iOSApplicationExtension 14.0, iOS 14.0, *)
|
|
struct Provider: IntentTimelineProvider {
|
|
public typealias Entry = SimpleEntry
|
|
|
|
func placeholder(in context: Context) -> SimpleEntry {
|
|
return SimpleEntry(date: Date(), contents: .recent)
|
|
}
|
|
|
|
func getSnapshot(for configuration: SelectFriendsIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {
|
|
let entry = SimpleEntry(date: Date(), contents: context.isPreview ? .preview : .peers(ParsedPeers(accountId: 0, peers: WidgetDataPeers(accountPeerId: 0, peers: [], updateTimestamp: 0))))
|
|
completion(entry)
|
|
}
|
|
|
|
func getTimeline(for configuration: SelectFriendsIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
|
|
getCommonTimeline(friends: configuration.friends, in: context, completion: completion)
|
|
}
|
|
}
|
|
|
|
@available(iOSApplicationExtension 14.0, iOS 14.0, *)
|
|
struct AvatarsProvider: IntentTimelineProvider {
|
|
public typealias Entry = SimpleEntry
|
|
|
|
func placeholder(in context: Context) -> SimpleEntry {
|
|
return SimpleEntry(date: Date(), contents: .recent)
|
|
}
|
|
|
|
func getSnapshot(for configuration: SelectAvatarFriendsIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {
|
|
let entry = SimpleEntry(date: Date(), contents: context.isPreview ? .preview : .peers(ParsedPeers(accountId: 0, peers: WidgetDataPeers(accountPeerId: 0, peers: [], updateTimestamp: 0))))
|
|
completion(entry)
|
|
}
|
|
|
|
func getTimeline(for configuration: SelectAvatarFriendsIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
|
|
getCommonTimeline(friends: configuration.friends, in: context, completion: completion)
|
|
}
|
|
}
|
|
|
|
@available(iOSApplicationExtension 14.0, iOS 14.0, *)
|
|
struct SimpleEntry: TimelineEntry {
|
|
enum Contents {
|
|
case recent
|
|
case preview
|
|
case peers(ParsedPeers)
|
|
}
|
|
|
|
let date: Date
|
|
let contents: Contents
|
|
}
|
|
|
|
enum PeersWidgetData {
|
|
case empty
|
|
case preview
|
|
case peers(ParsedPeers)
|
|
}
|
|
|
|
@available(iOSApplicationExtension 14.0, iOS 14.0, *)
|
|
struct AvatarItemView: View {
|
|
var peer: ParsedPeer?
|
|
var itemSize: CGFloat
|
|
var placeholderColor: Color
|
|
|
|
var body: some View {
|
|
return ZStack {
|
|
if let peer = peer {
|
|
Image(uiImage: avatarImage(accountPeerId: peer.accountPeerId, peer: peer.peer, size: CGSize(width: itemSize, height: itemSize)))
|
|
.clipShape(Circle())
|
|
} else {
|
|
Circle()
|
|
.fill(placeholderColor)
|
|
.frame(width: itemSize, height: itemSize)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@available(iOSApplicationExtension 14.0, iOS 14.0, *)
|
|
struct WidgetView: View {
|
|
@Environment(\.widgetFamily) private var widgetFamily
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
let data: PeersWidgetData
|
|
let presentationData: WidgetPresentationData
|
|
|
|
private func linkForPeer(accountId: Int64, id: Int64) -> String {
|
|
switch self.widgetFamily {
|
|
case .systemSmall:
|
|
return "\(buildConfig.appSpecificUrlScheme)://"
|
|
default:
|
|
return "\(buildConfig.appSpecificUrlScheme)://localpeer?id=\(id)&accountId=\(accountId)"
|
|
}
|
|
}
|
|
|
|
func chatTopLine(_ content: ChatContent) -> some View {
|
|
let dateText: String
|
|
|
|
let chatTitle: AnyView
|
|
let date: Text
|
|
var isPlaceholder = false
|
|
|
|
switch content {
|
|
case let .peer(peer):
|
|
if let message = peer.peer.message {
|
|
dateText = DateFormatter.localizedString(from: Date(timeIntervalSince1970: Double(message.timestamp)), dateStyle: .none, timeStyle: .short)
|
|
} else {
|
|
dateText = ""
|
|
}
|
|
var formattedName = peer.peer.name
|
|
if peer.accountPeerId == peer.peer.id {
|
|
formattedName = self.presentationData.chatSavedMessages
|
|
} else if let lastName = peer.peer.lastName {
|
|
formattedName.append(" \(lastName)")
|
|
}
|
|
chatTitle = AnyView(Text(formattedName)
|
|
.lineLimit(1)
|
|
.font(Font.system(size: 16.0, weight: .medium, design: .default))
|
|
.foregroundColor(.primary))
|
|
date = Text(dateText)
|
|
.font(Font.system(size: 14.0, weight: .regular, design: .default)).foregroundColor(.secondary)
|
|
case let .preview(index):
|
|
let titleText = index == 0 ? "News Channel" : "Duck"
|
|
dateText = index == 0 ? "9:00" : "8:42"
|
|
chatTitle = AnyView(Text(titleText)
|
|
.lineLimit(1)
|
|
.font(Font.system(size: 16.0, weight: .medium, design: .default))
|
|
.foregroundColor(.primary))
|
|
date = Text(dateText)
|
|
.font(Font.system(size: 14.0, weight: .regular, design: .default)).foregroundColor(.secondary)
|
|
case .placeholder:
|
|
isPlaceholder = true
|
|
dateText = " "
|
|
chatTitle = AnyView(Text(" ").font(Font.system(size: 16.0, weight: .medium, design: .default)).foregroundColor(.primary))
|
|
date = Text(dateText)
|
|
.font(Font.system(size: 16.0, weight: .regular, design: .default)).foregroundColor(.secondary)
|
|
}
|
|
return HStack(alignment: .center, spacing: 0.0, content: {
|
|
if !isPlaceholder {
|
|
chatTitle
|
|
} else {
|
|
chatTitle
|
|
.frame(minWidth: 48.0)
|
|
.background(GeometryReader { geometry in
|
|
RoundedRectangle(cornerRadius: 4.0)
|
|
.fill(getPlaceholderColor())
|
|
.frame(width: geometry.size.width, height: 8.0, alignment: .center)
|
|
.offset(x: 0.0, y: (geometry.size.height - 8.0) / 2.0 + 1.0)
|
|
}
|
|
)
|
|
}
|
|
Spacer()
|
|
if !isPlaceholder {
|
|
date
|
|
} else {
|
|
date
|
|
.background(GeometryReader { geometry in
|
|
RoundedRectangle(cornerRadius: 4.0)
|
|
.fill(getPlaceholderColor())
|
|
.frame(width: geometry.size.width, height: 8.0, alignment: .center)
|
|
.offset(x: 0.0, y: (geometry.size.height - 8.0) / 2.0)
|
|
}
|
|
)
|
|
}
|
|
})
|
|
.padding(0.0)
|
|
}
|
|
|
|
func chatBottomLine(_ content: ChatContent) -> AnyView {
|
|
var text = ""
|
|
var isPlaceholder = false
|
|
switch content {
|
|
case let .peer(peer):
|
|
if let message = peer.peer.message {
|
|
text = message.text
|
|
switch message.content {
|
|
case .text:
|
|
break
|
|
case .image:
|
|
if !message.text.isEmpty {
|
|
text = "🖼 \(message.text)"
|
|
} else {
|
|
text = "🖼 \(self.presentationData.messagePhoto)"
|
|
}
|
|
case .video:
|
|
if !message.text.isEmpty {
|
|
text = "📹 \(message.text)"
|
|
} else {
|
|
text = "📹 \(self.presentationData.messageVideo)"
|
|
}
|
|
case .gif:
|
|
if !message.text.isEmpty {
|
|
text = "\(message.text)"
|
|
} else {
|
|
text = "\(self.presentationData.messageAnimation)"
|
|
}
|
|
case let .file(file):
|
|
if !message.text.isEmpty {
|
|
text = "📹 \(message.text)"
|
|
} else {
|
|
text = "📎 \(file.name)"
|
|
}
|
|
case let .music(music):
|
|
if !music.title.isEmpty && !music.artist.isEmpty {
|
|
text = "\(music.artist) — \(music.title)"
|
|
} else if !music.title.isEmpty {
|
|
text = music.title
|
|
} else if !music.artist.isEmpty {
|
|
text = music.artist
|
|
} else {
|
|
text = "Unknown Artist"
|
|
}
|
|
case .voiceMessage:
|
|
text = "🎤 \(self.presentationData.messageVoice)"
|
|
case .videoMessage:
|
|
text = "\(self.presentationData.messageVideoMessage)"
|
|
case let .sticker(sticker):
|
|
text = "\(sticker.altText) \(presentationData.messageSticker)"
|
|
case let .call(call):
|
|
if call.isVideo {
|
|
text = "\(self.presentationData.messageVideoCall)"
|
|
} else {
|
|
text = "\(self.presentationData.messageVoiceCall)"
|
|
}
|
|
case .mapLocation:
|
|
text = "\(self.presentationData.messageLocation)"
|
|
case let .game(game):
|
|
text = "🎮 \(game.title)"
|
|
case let .poll(poll):
|
|
text = "📊 \(poll.title)"
|
|
case let .autodeleteTimer(value):
|
|
if value.value != nil {
|
|
text = self.presentationData.autodeleteTimerUpdated
|
|
} else {
|
|
text = self.presentationData.autodeleteTimerRemoved
|
|
}
|
|
}
|
|
|
|
if let author = message.author {
|
|
if author.isMe {
|
|
text = "\(presentationData.messageAuthorYou): \(text)"
|
|
} else {
|
|
text = "\(author.title): \(text)"
|
|
}
|
|
}
|
|
}
|
|
text += "\n"
|
|
case let .preview(index):
|
|
if index == 0 {
|
|
text = "☀️ 23 °C\n☁️ Passing Clouds"
|
|
} else {
|
|
text = "😂 \(presentationData.messageSticker)"
|
|
text += "\n"
|
|
}
|
|
case .placeholder:
|
|
isPlaceholder = true
|
|
}
|
|
|
|
let textView = Text(text)
|
|
.lineLimit(2)
|
|
.font(Font.system(size: 15.0, weight: .regular, design: .default))
|
|
.foregroundColor(.secondary)
|
|
.multilineTextAlignment(.leading)
|
|
.padding(0.0)
|
|
|
|
if !isPlaceholder {
|
|
return AnyView(textView)
|
|
} else {
|
|
return AnyView(
|
|
VStack(alignment: .leading, spacing: 0.0, content: {
|
|
Text(" ")
|
|
.lineLimit(1)
|
|
.font(Font.system(size: 15.0, weight: .regular, design: .default))
|
|
.foregroundColor(.secondary)
|
|
.multilineTextAlignment(.leading)
|
|
.padding(0.0)
|
|
.frame(minWidth: 182.0)
|
|
.background(GeometryReader { geometry in
|
|
RoundedRectangle(cornerRadius: 4.0)
|
|
.fill(getPlaceholderColor())
|
|
.frame(width: geometry.size.width, height: 8.0, alignment: .center)
|
|
.offset(x: 0.0, y: (geometry.size.height - 8.0) / 2.0 - 1.0)
|
|
}
|
|
)
|
|
Text(" ")
|
|
.lineLimit(1)
|
|
.font(Font.system(size: 15.0, weight: .regular, design: .default))
|
|
.foregroundColor(.secondary)
|
|
.multilineTextAlignment(.leading)
|
|
.padding(0.0)
|
|
.frame(minWidth: 96.0)
|
|
.background(GeometryReader { geometry in
|
|
RoundedRectangle(cornerRadius: 4.0)
|
|
.fill(getPlaceholderColor())
|
|
.frame(width: geometry.size.width, height: 8.0, alignment: .center)
|
|
.offset(x: 0.0, y: (geometry.size.height - 8.0) / 2.0 - 1.0)
|
|
}
|
|
)
|
|
})
|
|
)
|
|
}
|
|
}
|
|
|
|
enum ChatContent {
|
|
case peer(ParsedPeer)
|
|
case preview(Int)
|
|
case placeholder
|
|
}
|
|
|
|
func chatContent(_ content: ChatContent) -> some View {
|
|
return VStack(alignment: .leading, spacing: 0.0, content: {
|
|
chatTopLine(content)
|
|
chatBottomLine(content)
|
|
})
|
|
}
|
|
|
|
func chatContentView(_ index: Int, size: CGSize) -> AnyView {
|
|
let peers: ParsedPeers?
|
|
var isPlaceholder = false
|
|
var isPreview = false
|
|
switch data {
|
|
case let .peers(peersValue):
|
|
if peersValue.peers.count <= index {
|
|
isPlaceholder = peersValue.peers.count != 0
|
|
peers = nil
|
|
} else {
|
|
peers = peersValue
|
|
}
|
|
case .preview:
|
|
peers = nil
|
|
isPreview = true
|
|
default:
|
|
peers = nil
|
|
}
|
|
|
|
let itemHeight = (size.height - 22.0) / 2.0
|
|
|
|
if isPlaceholder {
|
|
return AnyView(Spacer()
|
|
.frame(width: size.width, height: itemHeight, alignment: .leading)
|
|
)
|
|
}
|
|
|
|
if isPreview {
|
|
return AnyView(
|
|
HStack(alignment: .center, spacing: 0.0, content: {
|
|
Image("Widget/Avatar\(index == 0 ? "Channel" : "1")")
|
|
.aspectRatio(1.0, contentMode: .fit)
|
|
.clipShape(Circle())
|
|
.frame(width: 54.0, height: 54.0, alignment: .leading)
|
|
.padding(EdgeInsets(top: 0.0, leading: 10.0, bottom: 0.0, trailing: 10.0))
|
|
chatContent(.preview(index)).frame(maxWidth: .infinity).padding(EdgeInsets(top: 0.0, leading: 0.0, bottom: 0.0, trailing: 10.0))
|
|
})
|
|
.frame(width: size.width, height: itemHeight, alignment: .leading)
|
|
)
|
|
}
|
|
|
|
let url: URL
|
|
if let peers = peers {
|
|
url = URL(string: linkForPeer(accountId: peers.peers[index].accountId, id: peers.peers[index].peer.id))!
|
|
} else {
|
|
url = URL(string: "\(buildConfig.appSpecificUrlScheme)://")!
|
|
}
|
|
|
|
let content: ChatContent
|
|
if let peer = peers?.peers[index] {
|
|
content = .peer(peer)
|
|
} else {
|
|
content = .placeholder
|
|
}
|
|
|
|
let avatarView: AvatarItemView
|
|
avatarView = AvatarItemView(peer: peers?.peers[index], itemSize: 54.0, placeholderColor: getPlaceholderColor())
|
|
|
|
return AnyView(
|
|
Link(destination: url, label: {
|
|
HStack(alignment: .center, spacing: 0.0, content: {
|
|
avatarView
|
|
.frame(width: 54.0, height: 54.0, alignment: .leading)
|
|
.padding(EdgeInsets(top: 0.0, leading: 10.0, bottom: 0.0, trailing: 10.0))
|
|
chatContent(content).frame(maxWidth: .infinity).padding(EdgeInsets(top: 0.0, leading: 0.0, bottom: 0.0, trailing: 10.0))
|
|
})
|
|
})
|
|
.frame(width: size.width, height: itemHeight, alignment: .leading)
|
|
)
|
|
}
|
|
|
|
func chatSeparatorView(size: CGSize) -> some View {
|
|
return HStack(alignment: .center, spacing: 0.0, content: {
|
|
Spacer()
|
|
Rectangle()
|
|
.foregroundColor(getSeparatorColor())
|
|
.frame(width: size.width - 54.0 - 20.0, height: 0.5, alignment: .leading)
|
|
})
|
|
.frame(width: size.width, height: 1.0, alignment: .leading)
|
|
}
|
|
|
|
func chatsUpdateBackgroundView(size: CGSize) -> some View {
|
|
return Rectangle().foregroundColor(getUpdatedBackgroundColor())
|
|
//.position(x: size.width / 2.0, y: size.height - 11.0)
|
|
.frame(width: size.width, height: 22.0, alignment: .center)
|
|
}
|
|
|
|
func chatUpdateView(size: CGSize) -> some View {
|
|
return ZStack(alignment: Alignment(horizontal: .center, vertical: .center), content: {
|
|
Rectangle().foregroundColor(getUpdatedBackgroundColor())
|
|
chatsUpdateTimestampView(size: size)
|
|
})
|
|
.frame(width: size.width, height: 22.0 - 1.0, alignment: .center)
|
|
}
|
|
|
|
func chatsUpdateTimestampView(size: CGSize) -> some View {
|
|
let text: String
|
|
switch data {
|
|
case let .peers(peersValue):
|
|
if peersValue.peers.isEmpty {
|
|
text = self.presentationData.widgetLongTapToEdit
|
|
} else {
|
|
let date = Date(timeIntervalSince1970: Double(peersValue.updateTimestamp))
|
|
let calendar = Calendar.current
|
|
if !calendar.isDate(Date(), inSameDayAs: date) {
|
|
let formatter = DateFormatter()
|
|
formatter.dateStyle = .short
|
|
formatter.timeStyle = .none
|
|
text = self.presentationData.widgetUpdatedAt.replacingOccurrences(of: "{}", with: formatter.string(from: date))
|
|
} else {
|
|
let formatter = DateFormatter()
|
|
formatter.dateStyle = .none
|
|
formatter.timeStyle = .short
|
|
text = self.presentationData.widgetUpdatedTodayAt.replacingOccurrences(of: "{}", with: formatter.string(from: date))
|
|
}
|
|
}
|
|
case .preview:
|
|
let date = Date()
|
|
let calendar = Calendar.current
|
|
if !calendar.isDate(Date(), inSameDayAs: date) {
|
|
let formatter = DateFormatter()
|
|
formatter.dateStyle = .short
|
|
formatter.timeStyle = .none
|
|
text = self.presentationData.widgetUpdatedAt.replacingOccurrences(of: "{}", with: formatter.string(from: date))
|
|
} else {
|
|
let formatter = DateFormatter()
|
|
formatter.dateStyle = .none
|
|
formatter.timeStyle = .short
|
|
text = self.presentationData.widgetUpdatedTodayAt.replacingOccurrences(of: "{}", with: formatter.string(from: date))
|
|
}
|
|
default:
|
|
text = self.presentationData.widgetLongTapToEdit
|
|
}
|
|
|
|
return Text(text)
|
|
.font(Font.system(size: 12.0))
|
|
.foregroundColor(getUpdatedTextColor())
|
|
}
|
|
|
|
func getBackgroundColor() -> Color {
|
|
switch colorScheme {
|
|
case .light:
|
|
return .white
|
|
case .dark:
|
|
return Color(.sRGB, red: 28.0 / 255.0, green: 28.0 / 255.0, blue: 30.0 / 255.0, opacity: 1.0)
|
|
@unknown default:
|
|
return .secondary
|
|
}
|
|
}
|
|
|
|
func getSeparatorColor() -> Color {
|
|
switch colorScheme {
|
|
case .light:
|
|
return Color(.sRGB, red: 216.0 / 255.0, green: 216.0 / 255.0, blue: 216.0 / 255.0, opacity: 1.0)
|
|
case .dark:
|
|
return Color(.sRGB, red: 0.0 / 255.0, green: 0.0 / 255.0, blue: 0.0 / 255.0, opacity: 1.0)
|
|
@unknown default:
|
|
return .secondary
|
|
}
|
|
}
|
|
|
|
func getUpdatedBackgroundColor() -> Color {
|
|
switch colorScheme {
|
|
case .light:
|
|
return Color(.sRGB, red: 242.0 / 255.0, green: 242.0 / 255.0, blue: 247.0 / 255.0, opacity: 1.0)
|
|
case .dark:
|
|
return Color(.sRGB, red: 21.0 / 255.0, green: 21.0 / 255.0, blue: 21.0 / 255.0, opacity: 1.0)
|
|
@unknown default:
|
|
return .secondary
|
|
}
|
|
}
|
|
|
|
func getUpdatedTextColor() -> Color {
|
|
switch colorScheme {
|
|
case .light:
|
|
return Color(.sRGB, red: 142.0 / 255.0, green: 142.0 / 255.0, blue: 146.0 / 255.0, opacity: 1.0)
|
|
case .dark:
|
|
return Color(.sRGB, red: 142.0 / 255.0, green: 142.0 / 255.0, blue: 146.0 / 255.0, opacity: 1.0)
|
|
@unknown default:
|
|
return .secondary
|
|
}
|
|
}
|
|
|
|
func getPlaceholderColor() -> Color {
|
|
switch colorScheme {
|
|
case .light:
|
|
return Color(.sRGB, red: 235.0 / 255.0, green: 235.0 / 255.0, blue: 241.0 / 255.0, opacity: 1.0)
|
|
case .dark:
|
|
return Color(.sRGB, red: 38.0 / 255.0, green: 38.0 / 255.0, blue: 41.0 / 255.0, opacity: 1.0)
|
|
@unknown default:
|
|
return .secondary
|
|
}
|
|
}
|
|
|
|
var body: some View {
|
|
GeometryReader(content: { geometry in
|
|
return VStack(alignment: .center, spacing: 0.0, content: {
|
|
chatContentView(0, size: geometry.size)
|
|
chatSeparatorView(size: geometry.size)
|
|
chatContentView(1, size: geometry.size)
|
|
chatUpdateView(size: geometry.size)
|
|
})
|
|
})
|
|
.background(Rectangle().foregroundColor(getBackgroundColor()))
|
|
.padding(0.0)
|
|
.unredacted()
|
|
}
|
|
}
|
|
|
|
@available(iOSApplicationExtension 14.0, iOS 14.0, *)
|
|
struct AvatarsWidgetView: View {
|
|
@Environment(\.widgetFamily) private var widgetFamily
|
|
@Environment(\.colorScheme) private var colorScheme
|
|
let data: PeersWidgetData
|
|
let presentationData: WidgetPresentationData
|
|
|
|
func placeholder(geometry: GeometryProxy) -> some View {
|
|
return Spacer()
|
|
}
|
|
|
|
private func linkForPeer(accountId: Int64, id: Int64) -> String {
|
|
switch self.widgetFamily {
|
|
case .systemSmall:
|
|
return "\(buildConfig.appSpecificUrlScheme)://"
|
|
default:
|
|
return "\(buildConfig.appSpecificUrlScheme)://localpeer?id=\(id)&accountId=\(accountId)"
|
|
}
|
|
}
|
|
|
|
func getPlaceholderColor() -> Color {
|
|
switch colorScheme {
|
|
case .light:
|
|
return Color(.sRGB, red: 235.0 / 255.0, green: 235.0 / 255.0, blue: 241.0 / 255.0, opacity: 1.0)
|
|
case .dark:
|
|
return Color(.sRGB, red: 38.0 / 255.0, green: 38.0 / 255.0, blue: 41.0 / 255.0, opacity: 1.0)
|
|
@unknown default:
|
|
return .secondary
|
|
}
|
|
}
|
|
|
|
func itemView(index: Int) -> some View {
|
|
let peers: ParsedPeers?
|
|
var isPlaceholder = false
|
|
var isPreview = false
|
|
switch data {
|
|
case let .peers(peersValue):
|
|
if peersValue.peers.count <= index {
|
|
isPlaceholder = peersValue.peers.count != 0
|
|
peers = nil
|
|
} else {
|
|
peers = peersValue
|
|
}
|
|
case .preview:
|
|
peers = nil
|
|
isPreview = true
|
|
default:
|
|
peers = nil
|
|
}
|
|
|
|
if let peers = peers {
|
|
return AnyView(Link(destination: URL(string: linkForPeer(accountId: peers.peers[index].accountId, id: peers.peers[index].peer.id))!, label: {
|
|
GeometryReader(content: { geometry in
|
|
AvatarItemView(peer: peers.peers[index], itemSize: geometry.size.height, placeholderColor: getPlaceholderColor())
|
|
})
|
|
}).aspectRatio(1.0, contentMode: .fit))
|
|
} else if isPlaceholder {
|
|
return AnyView(Circle().aspectRatio(1.0, contentMode: .fit).foregroundColor(getPlaceholderColor()))
|
|
} else if isPreview {
|
|
return AnyView(Image("Widget/Avatar\(index + 1)").aspectRatio(1.0, contentMode: .fit).clipShape(Circle()))
|
|
} else {
|
|
return AnyView(Circle().aspectRatio(1.0, contentMode: .fit).foregroundColor(getPlaceholderColor()))
|
|
}
|
|
}
|
|
|
|
var body: some View {
|
|
return VStack(alignment: .center, spacing: 18.0, content: {
|
|
HStack(alignment: .center, spacing: nil, content: {
|
|
ForEach(0 ..< 4, id: \.self) { index in
|
|
itemView(index: index)
|
|
if index != 3 {
|
|
Spacer()
|
|
}
|
|
}
|
|
})
|
|
HStack(alignment: .center, spacing: nil, content: {
|
|
ForEach(0 ..< 4, id: \.self) { index in
|
|
itemView(index: 4 + index)
|
|
if index != 3 {
|
|
Spacer()
|
|
}
|
|
}
|
|
})
|
|
})
|
|
.padding(EdgeInsets(top: 10.0, leading: 10.0, bottom: 10.0, trailing: 10.0))
|
|
.unredacted()
|
|
}
|
|
}
|
|
|
|
private let buildConfig: BuildConfig = {
|
|
let appBundleIdentifier = Bundle.main.bundleIdentifier!
|
|
guard let lastDotRange = appBundleIdentifier.range(of: ".", options: [.backwards]) else {
|
|
preconditionFailure()
|
|
}
|
|
let baseAppBundleId = String(appBundleIdentifier[..<lastDotRange.lowerBound])
|
|
|
|
let buildConfig = BuildConfig(baseAppBundleId: baseAppBundleId)
|
|
return buildConfig
|
|
}()
|
|
|
|
@available(iOSApplicationExtension 14.0, iOS 14.0, *)
|
|
func getWidgetData(contents: SimpleEntry.Contents) -> PeersWidgetData {
|
|
switch contents {
|
|
case .recent:
|
|
return .empty
|
|
case .preview:
|
|
return .preview
|
|
case let .peers(peers):
|
|
return .peers(peers)
|
|
}
|
|
}
|
|
|
|
@available(iOSApplicationExtension 14.0, iOS 14.0, *)
|
|
struct Static_Widget: Widget {
|
|
private let kind: String = "Static_Widget"
|
|
|
|
public var body: some WidgetConfiguration {
|
|
let presentationData = WidgetPresentationData.getForExtension()
|
|
|
|
return IntentConfiguration(kind: kind, intent: SelectFriendsIntent.self, provider: Provider(), content: { entry in
|
|
WidgetView(data: getWidgetData(contents: entry.contents), presentationData: presentationData)
|
|
})
|
|
.supportedFamilies([.systemMedium])
|
|
.configurationDisplayName(presentationData.widgetChatsGalleryTitle)
|
|
.description(presentationData.widgetChatsGalleryDescription)
|
|
}
|
|
}
|
|
|
|
@available(iOSApplicationExtension 14.0, iOS 14.0, *)
|
|
struct Static_AvatarsWidget: Widget {
|
|
private let kind: String = "Static_AvatarsWidget"
|
|
|
|
public var body: some WidgetConfiguration {
|
|
let presentationData = WidgetPresentationData.getForExtension()
|
|
|
|
return IntentConfiguration(kind: kind, intent: SelectAvatarFriendsIntent.self, provider: AvatarsProvider(), content: { entry in
|
|
AvatarsWidgetView(data: getWidgetData(contents: entry.contents), presentationData: presentationData)
|
|
})
|
|
.supportedFamilies([.systemMedium])
|
|
.configurationDisplayName(presentationData.widgetShortcutsGalleryTitle)
|
|
.description(presentationData.widgetShortcutsGalleryDescription)
|
|
}
|
|
}
|
|
|
|
@main
|
|
struct AllWidgetsEntryPoint {
|
|
static func main() {
|
|
if #available(iOS 14.0, *) {
|
|
AllWidgets.main()
|
|
} else {
|
|
preconditionFailure()
|
|
}
|
|
}
|
|
}
|
|
|
|
@available(iOSApplicationExtension 14.0, iOS 14.0, *)
|
|
struct AllWidgets: WidgetBundle {
|
|
var body: some Widget {
|
|
Static_Widget()
|
|
Static_AvatarsWidget()
|
|
}
|
|
}
|
|
|
|
#else
|
|
|
|
@main
|
|
class MyApp {
|
|
static func main() {
|
|
preconditionFailure("Not supported")
|
|
}
|
|
}
|
|
|
|
#endif
|