mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-05-29 13:34:20 +00:00
Merge branch 'master' into lp/floating-buttons-date
This commit is contained in:
@@ -102,7 +102,7 @@ extension ThemeWallpaper {
|
||||
public func importFromString() -> ThemeWallpaper {
|
||||
if preset == nil, let image {
|
||||
// Need to save image from string and to save its path
|
||||
if let parsed = UIImage(base64Encoded: image),
|
||||
if let parsed = imageFromBase64(image),
|
||||
let filename = saveWallpaperFile(image: parsed) {
|
||||
var copy = self
|
||||
copy.image = nil
|
||||
|
||||
@@ -46,7 +46,7 @@ struct CIGroupInvitationView: View {
|
||||
.foregroundColor(inProgress ? theme.colors.secondary : chatIncognito ? .indigo : theme.colors.primary)
|
||||
.font(.callout)
|
||||
+ Text(" ")
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||
)
|
||||
.overlay(DetermineWidth())
|
||||
}
|
||||
@@ -54,7 +54,7 @@ struct CIGroupInvitationView: View {
|
||||
(
|
||||
groupInvitationText()
|
||||
+ Text(" ")
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showStatus: false, showEdited: false, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||
)
|
||||
.overlay(DetermineWidth())
|
||||
}
|
||||
|
||||
@@ -165,9 +165,9 @@ struct CIImageView: View {
|
||||
private func fileIcon(_ icon: String, _ size: CGFloat, _ padding: CGFloat) -> some View {
|
||||
Image(systemName: icon)
|
||||
.resizable()
|
||||
.invertedForegroundStyle()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: size, height: size)
|
||||
.foregroundColor(.white)
|
||||
.padding(padding)
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ struct CILinkView: View {
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .center, spacing: 6) {
|
||||
if let uiImage = UIImage(base64Encoded: linkPreview.image) {
|
||||
if let uiImage = imageFromBase64(linkPreview.image) {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
|
||||
@@ -18,6 +18,7 @@ struct CIMetaView: View {
|
||||
var paleMetaColor = Color(UIColor.tertiaryLabel)
|
||||
var showStatus = true
|
||||
var showEdited = true
|
||||
var invertedMaterial = false
|
||||
|
||||
@AppStorage(DEFAULT_SHOW_SENT_VIA_RPOXY) private var showSentViaProxy = false
|
||||
|
||||
@@ -25,18 +26,59 @@ struct CIMetaView: View {
|
||||
if chatItem.isDeletedContent {
|
||||
chatItem.timestampText.font(.caption).foregroundColor(metaColor)
|
||||
} else {
|
||||
ciMetaText(
|
||||
chatItem.meta,
|
||||
chatTTL: chat.chatInfo.timedMessagesTTL,
|
||||
encrypted: chatItem.encryptedFile,
|
||||
color: chatItem.meta.itemStatus.sndProgress == .partial
|
||||
? paleMetaColor
|
||||
: metaColor,
|
||||
showStatus: showStatus,
|
||||
showEdited: showEdited,
|
||||
showViaProxy: showSentViaProxy,
|
||||
showTimesamp: showTimestamp
|
||||
)
|
||||
ZStack {
|
||||
ciMetaText(
|
||||
chatItem.meta,
|
||||
chatTTL: chat.chatInfo.timedMessagesTTL,
|
||||
encrypted: chatItem.encryptedFile,
|
||||
color: metaColor,
|
||||
paleColor: paleMetaColor,
|
||||
colorMode: invertedMaterial
|
||||
? .invertedMaterial
|
||||
: .normal,
|
||||
showStatus: showStatus,
|
||||
showEdited: showEdited,
|
||||
showViaProxy: showSentViaProxy,
|
||||
showTimesamp: showTimestamp
|
||||
).invertedForegroundStyle(enabled: invertedMaterial)
|
||||
if invertedMaterial {
|
||||
ciMetaText(
|
||||
chatItem.meta,
|
||||
chatTTL: chat.chatInfo.timedMessagesTTL,
|
||||
encrypted: chatItem.encryptedFile,
|
||||
colorMode: .normal,
|
||||
onlyOverrides: true,
|
||||
showStatus: showStatus,
|
||||
showEdited: showEdited,
|
||||
showViaProxy: showSentViaProxy,
|
||||
showTimesamp: showTimestamp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum MetaColorMode {
|
||||
// Renders provided colours
|
||||
case normal
|
||||
// Fully transparent meta - used for reserving space
|
||||
case transparent
|
||||
// Renders white on dark backgrounds and black on light ones
|
||||
case invertedMaterial
|
||||
|
||||
func resolve(_ c: Color?) -> Color? {
|
||||
switch self {
|
||||
case .normal: c
|
||||
case .transparent: .clear
|
||||
case .invertedMaterial: nil
|
||||
}
|
||||
}
|
||||
|
||||
var statusSpacer: Text {
|
||||
switch self {
|
||||
case .normal, .transparent: Text(Image(systemName: "circlebadge.fill")).foregroundColor(.clear)
|
||||
case .invertedMaterial: Text(" ").kerning(13)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -45,47 +87,77 @@ func ciMetaText(
|
||||
_ meta: CIMeta,
|
||||
chatTTL: Int?,
|
||||
encrypted: Bool?,
|
||||
color: Color = .clear,
|
||||
color: Color = .clear, // we use this function to reserve space without rendering meta
|
||||
paleColor: Color? = nil,
|
||||
primaryColor: Color = .accentColor,
|
||||
transparent: Bool = false,
|
||||
colorMode: MetaColorMode = .normal,
|
||||
onlyOverrides: Bool = false, // only render colors that differ from base
|
||||
showStatus: Bool = true,
|
||||
showEdited: Bool = true,
|
||||
showViaProxy: Bool,
|
||||
showTimesamp: Bool
|
||||
) -> Text {
|
||||
var r = Text("")
|
||||
var space: Text? = nil
|
||||
let appendSpace = {
|
||||
if let sp = space {
|
||||
r = r + sp
|
||||
space = nil
|
||||
}
|
||||
}
|
||||
let resolved = colorMode.resolve(color)
|
||||
if showEdited, meta.itemEdited {
|
||||
r = r + statusIconText("pencil", color)
|
||||
r = r + statusIconText("pencil", resolved)
|
||||
}
|
||||
if meta.disappearing {
|
||||
r = r + statusIconText("timer", color).font(.caption2)
|
||||
r = r + statusIconText("timer", resolved).font(.caption2)
|
||||
let ttl = meta.itemTimed?.ttl
|
||||
if ttl != chatTTL {
|
||||
r = r + Text(shortTimeText(ttl)).foregroundColor(color)
|
||||
r = r + colored(Text(shortTimeText(ttl)), resolved)
|
||||
}
|
||||
r = r + Text(" ")
|
||||
space = Text(" ")
|
||||
}
|
||||
if showViaProxy, meta.sentViaProxy == true {
|
||||
r = r + statusIconText("arrow.forward", color.opacity(0.67)).font(.caption2)
|
||||
appendSpace()
|
||||
r = r + statusIconText("arrow.forward", resolved?.opacity(0.67)).font(.caption2)
|
||||
}
|
||||
if showStatus {
|
||||
if let (image, statusColor) = meta.itemStatus.statusIcon(color, primaryColor) {
|
||||
r = r + Text(image).foregroundColor(transparent ? .clear : statusColor) + Text(" ")
|
||||
appendSpace()
|
||||
if let (image, statusColor) = meta.itemStatus.statusIcon(color, paleColor ?? color, primaryColor) {
|
||||
let metaColor = if onlyOverrides && statusColor == color {
|
||||
Color.clear
|
||||
} else {
|
||||
colorMode.resolve(statusColor)
|
||||
}
|
||||
r = r + colored(Text(image), metaColor)
|
||||
space = Text(" ")
|
||||
} else if !meta.disappearing {
|
||||
r = r + statusIconText("circlebadge.fill", .clear) + Text(" ")
|
||||
space = colorMode.statusSpacer + Text(" ")
|
||||
}
|
||||
}
|
||||
if let enc = encrypted {
|
||||
r = r + statusIconText(enc ? "lock" : "lock.open", color) + Text(" ")
|
||||
appendSpace()
|
||||
r = r + statusIconText(enc ? "lock" : "lock.open", resolved)
|
||||
space = Text(" ")
|
||||
}
|
||||
if showTimesamp {
|
||||
r = r + meta.timestampText.foregroundColor(color)
|
||||
appendSpace()
|
||||
r = r + colored(meta.timestampText, resolved)
|
||||
}
|
||||
return r.font(.caption)
|
||||
}
|
||||
|
||||
private func statusIconText(_ icon: String, _ color: Color) -> Text {
|
||||
Text(Image(systemName: icon)).foregroundColor(color)
|
||||
private func statusIconText(_ icon: String, _ color: Color?) -> Text {
|
||||
colored(Text(Image(systemName: icon)), color)
|
||||
}
|
||||
|
||||
// Applying `foregroundColor(nil)` breaks `.invertedForegroundStyle` modifier
|
||||
private func colored(_ t: Text, _ color: Color?) -> Text {
|
||||
if let color {
|
||||
t.foregroundColor(color)
|
||||
} else {
|
||||
t
|
||||
}
|
||||
}
|
||||
|
||||
struct CIMetaView_Previews: PreviewProvider {
|
||||
|
||||
@@ -126,7 +126,7 @@ struct CIRcvDecryptionError: View {
|
||||
.foregroundColor(syncSupported ? theme.colors.primary : theme.colors.secondary)
|
||||
.font(.callout)
|
||||
+ Text(" ")
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||
)
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
@@ -145,7 +145,7 @@ struct CIRcvDecryptionError: View {
|
||||
.foregroundColor(.red)
|
||||
.italic()
|
||||
+ Text(" ")
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||
+ ciMetaText(chatItem.meta, chatTTL: nil, encrypted: nil, colorMode: .transparent, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||
}
|
||||
.padding(.horizontal, 12)
|
||||
CIMetaView(chat: chat, chatItem: chatItem, metaColor: theme.colors.secondary)
|
||||
|
||||
@@ -292,30 +292,22 @@ struct CIVideoView: View {
|
||||
.clipShape(Circle())
|
||||
}
|
||||
|
||||
private func durationProgress() -> some View {
|
||||
HStack {
|
||||
Text("\(durationText(videoPlaying ? progress : duration))")
|
||||
.foregroundColor(.white)
|
||||
.font(.caption)
|
||||
.padding(.vertical, 3)
|
||||
.padding(.horizontal, 6)
|
||||
.background(Color.black.opacity(0.35))
|
||||
.cornerRadius(10)
|
||||
.padding([.top, .leading], 6)
|
||||
|
||||
if let file = chatItem.file, !videoPlaying {
|
||||
Text("\(ByteCountFormatter.string(fromByteCount: file.fileSize, countStyle: .binary))")
|
||||
.foregroundColor(.white)
|
||||
.font(.caption)
|
||||
.padding(.vertical, 3)
|
||||
.padding(.horizontal, 6)
|
||||
.background(Color.black.opacity(0.35))
|
||||
.cornerRadius(10)
|
||||
.padding(.top, 6)
|
||||
}
|
||||
private var fileSizeString: String {
|
||||
if let file = chatItem.file, !videoPlaying {
|
||||
" " + ByteCountFormatter.string(fromByteCount: file.fileSize, countStyle: .binary)
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
private func durationProgress() -> some View {
|
||||
Text((durationText(videoPlaying ? progress : duration)) + fileSizeString)
|
||||
.invertedForegroundStyle()
|
||||
.font(.caption)
|
||||
.padding(.vertical, 6)
|
||||
.padding(.horizontal, 12)
|
||||
}
|
||||
|
||||
private func imageView(_ img: UIImage) -> some View {
|
||||
let w = img.size.width <= img.size.height ? maxWidth * 0.75 : maxWidth
|
||||
return ZStack(alignment: .topTrailing) {
|
||||
@@ -411,9 +403,9 @@ struct CIVideoView: View {
|
||||
private func fileIcon(_ icon: String, _ size: CGFloat, _ padding: CGFloat) -> some View {
|
||||
Image(systemName: icon)
|
||||
.resizable()
|
||||
.invertedForegroundStyle()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: size, height: size)
|
||||
.foregroundColor(.white)
|
||||
.padding(smallView ? 0 : padding)
|
||||
}
|
||||
|
||||
@@ -428,10 +420,8 @@ struct CIVideoView: View {
|
||||
private func progressCircle(_ progress: Int64, _ total: Int64) -> some View {
|
||||
Circle()
|
||||
.trim(from: 0, to: Double(progress) / Double(total))
|
||||
.stroke(
|
||||
Color(uiColor: .white),
|
||||
style: StrokeStyle(lineWidth: 2)
|
||||
)
|
||||
.stroke(style: StrokeStyle(lineWidth: 2))
|
||||
.invertedForegroundStyle()
|
||||
.rotationEffect(.degrees(-90))
|
||||
.frame(width: 16, height: 16)
|
||||
.padding([.trailing, .top], smallView ? 0 : 11)
|
||||
|
||||
@@ -64,12 +64,17 @@ struct FramedItemView: View {
|
||||
.overlay(DetermineWidth())
|
||||
}
|
||||
|
||||
if chatItem.content.msgContent != nil {
|
||||
CIMetaView(chat: chat, chatItem: chatItem, metaColor: useWhiteMetaColor ? Color.white : theme.colors.secondary)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.bottom, 6)
|
||||
.overlay(DetermineWidth())
|
||||
.accessibilityLabel("")
|
||||
if let content = chatItem.content.msgContent {
|
||||
CIMetaView(
|
||||
chat: chat,
|
||||
chatItem: chatItem,
|
||||
metaColor: theme.colors.secondary,
|
||||
invertedMaterial: useWhiteMetaColor
|
||||
)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.bottom, 6)
|
||||
.overlay(DetermineWidth())
|
||||
.accessibilityLabel("")
|
||||
}
|
||||
}
|
||||
.background { chatItemFrameColorMaybeImageOrVideo(chatItem, theme).modifier(ChatTailPadding()) }
|
||||
@@ -185,7 +190,7 @@ struct FramedItemView: View {
|
||||
let v = ZStack(alignment: .topTrailing) {
|
||||
switch (qi.content) {
|
||||
case let .image(_, image):
|
||||
if let uiImage = UIImage(base64Encoded: image) {
|
||||
if let uiImage = imageFromBase64(image) {
|
||||
ciQuotedMsgView(qi)
|
||||
.padding(.trailing, 70).frame(minWidth: msgWidth, alignment: .leading)
|
||||
Image(uiImage: uiImage)
|
||||
@@ -197,7 +202,7 @@ struct FramedItemView: View {
|
||||
ciQuotedMsgView(qi)
|
||||
}
|
||||
case let .video(_, image, _):
|
||||
if let uiImage = UIImage(base64Encoded: image) {
|
||||
if let uiImage = imageFromBase64(image) {
|
||||
ciQuotedMsgView(qi)
|
||||
.padding(.trailing, 70).frame(minWidth: msgWidth, alignment: .leading)
|
||||
Image(uiImage: uiImage)
|
||||
|
||||
@@ -85,7 +85,7 @@ struct MsgContentView: View {
|
||||
}
|
||||
|
||||
private func reserveSpaceForMeta(_ mt: CIMeta) -> Text {
|
||||
(rightToLeft ? Text("\n") : Text(" ")) + ciMetaText(mt, chatTTL: chat.chatInfo.timedMessagesTTL, encrypted: nil, transparent: true, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||
(rightToLeft ? Text("\n") : Text(" ")) + ciMetaText(mt, chatTTL: chat.chatInfo.timedMessagesTTL, encrypted: nil, colorMode: .transparent, showViaProxy: showSentViaProxy, showTimesamp: showTimestamp)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ struct ChatItemView: View {
|
||||
default: nil
|
||||
}
|
||||
}
|
||||
.flatMap { UIImage(base64Encoded: $0) }
|
||||
.flatMap { imageFromBase64($0) }
|
||||
let adjustedMaxWidth = {
|
||||
if let preview, preview.size.width <= preview.size.height {
|
||||
maxWidth * 0.75
|
||||
|
||||
@@ -18,7 +18,7 @@ struct ComposeImageView: View {
|
||||
var body: some View {
|
||||
HStack(alignment: .center, spacing: 8) {
|
||||
let imgs: [UIImage] = images.compactMap { image in
|
||||
UIImage(base64Encoded: image)
|
||||
imageFromBase64(image)
|
||||
}
|
||||
if imgs.count == 0 {
|
||||
ProgressView()
|
||||
|
||||
@@ -40,7 +40,7 @@ struct ComposeLinkView: View {
|
||||
|
||||
private func linkPreviewView(_ linkPreview: LinkPreview) -> some View {
|
||||
HStack(alignment: .center, spacing: 8) {
|
||||
if let uiImage = UIImage(base64Encoded: linkPreview.image) {
|
||||
if let uiImage = imageFromBase64(linkPreview.image) {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
|
||||
@@ -302,7 +302,7 @@ struct ChatPreviewView: View {
|
||||
case let .link(_, preview):
|
||||
smallContentPreview(size: dynamicMediaSize) {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
Image(uiImage: UIImage(base64Encoded: preview.image) ?? UIImage(systemName: "arrow.up.right")!)
|
||||
Image(uiImage: imageFromBase64(preview.image) ?? UIImage(systemName: "arrow.up.right")!)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: dynamicMediaSize, height: dynamicMediaSize)
|
||||
@@ -323,12 +323,12 @@ struct ChatPreviewView: View {
|
||||
}
|
||||
case let .image(_, image):
|
||||
smallContentPreview(size: dynamicMediaSize) {
|
||||
CIImageView(chatItem: ci, preview: UIImage(base64Encoded: image), maxWidth: dynamicMediaSize, smallView: true, showFullScreenImage: $showFullscreenGallery)
|
||||
CIImageView(chatItem: ci, preview: imageFromBase64(image), maxWidth: dynamicMediaSize, smallView: true, showFullScreenImage: $showFullscreenGallery)
|
||||
.environmentObject(ReverseListScrollModel())
|
||||
}
|
||||
case let .video(_,image, duration):
|
||||
smallContentPreview(size: dynamicMediaSize) {
|
||||
CIVideoView(chatItem: ci, preview: UIImage(base64Encoded: image), duration: duration, maxWidth: dynamicMediaSize, videoWidth: nil, smallView: true, showFullscreenPlayer: $showFullscreenGallery)
|
||||
CIVideoView(chatItem: ci, preview: imageFromBase64(image), duration: duration, maxWidth: dynamicMediaSize, videoWidth: nil, smallView: true, showFullscreenPlayer: $showFullscreenGallery)
|
||||
.environmentObject(ReverseListScrollModel())
|
||||
}
|
||||
case let .voice(_, duration):
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// Test.swift
|
||||
// SimpleX (iOS)
|
||||
//
|
||||
// Created by Levitating Pineapple on 31/08/2024.
|
||||
// Copyright © 2024 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
extension View {
|
||||
@ViewBuilder
|
||||
func invertedForegroundStyle(enabled: Bool = true) -> some View {
|
||||
if enabled {
|
||||
foregroundStyle(Material.ultraThin)
|
||||
.environment(\.colorScheme, .dark)
|
||||
.grayscale(1)
|
||||
.contrast(-20)
|
||||
} else { self }
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ struct ProfileImage: View {
|
||||
@AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var radius = defaultProfileImageCorner
|
||||
|
||||
var body: some View {
|
||||
if let uiImage = UIImage(base64Encoded: imageStr) {
|
||||
if let uiImage = imageFromBase64(imageStr) {
|
||||
clipProfileImage(Image(uiImage: uiImage), size: size, radius: radius, blurred: blurred)
|
||||
} else {
|
||||
let c = color.asAnotherColorFromSecondaryVariant(theme)
|
||||
|
||||
@@ -11,12 +11,18 @@ import SwiftUI
|
||||
func showShareSheet(items: [Any], completed: (() -> Void)? = nil) {
|
||||
let keyWindowScene = UIApplication.shared.connectedScenes.first { $0.activationState == .foregroundActive } as? UIWindowScene
|
||||
if let keyWindow = keyWindowScene?.windows.filter(\.isKeyWindow).first,
|
||||
let presentedViewController = keyWindow.rootViewController?.presentedViewController ?? keyWindow.rootViewController {
|
||||
let rootViewController = keyWindow.rootViewController {
|
||||
// Find the top-most presented view controller
|
||||
var topController = rootViewController
|
||||
while let presentedViewController = topController.presentedViewController {
|
||||
topController = presentedViewController
|
||||
}
|
||||
let activityViewController = UIActivityViewController(activityItems: items, applicationActivities: nil)
|
||||
if let completed = completed {
|
||||
let handler: UIActivityViewController.CompletionWithItemsHandler = { _,_,_,_ in completed() }
|
||||
activityViewController.completionWithItemsHandler = handler
|
||||
}
|
||||
presentedViewController.present(activityViewController, animated: true)
|
||||
activityViewController.completionWithItemsHandler = { _, _, _, _ in
|
||||
completed()
|
||||
}
|
||||
}
|
||||
topController.present(activityViewController, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,7 +196,7 @@ func chatContactType(chat: Chat) -> ContactType {
|
||||
case .contactRequest:
|
||||
return .request
|
||||
case let .direct(contact):
|
||||
if contact.activeConn == nil && contact.profile.contactLink != nil {
|
||||
if contact.activeConn == nil && contact.profile.contactLink != nil && contact.active {
|
||||
return .card
|
||||
} else if contact.chatDeleted {
|
||||
return .chatDeleted
|
||||
|
||||
@@ -104,7 +104,7 @@ class ShareModel: ObservableObject {
|
||||
// Decode base64 images on background thread
|
||||
let profileImages = chats.reduce(into: Dictionary<ChatInfo.ID, UIImage>()) { dict, chatData in
|
||||
if let profileImage = chatData.chatInfo.image,
|
||||
let uiImage = UIImage(base64Encoded: profileImage) {
|
||||
let uiImage = imageFromBase64(profileImage) {
|
||||
dict[chatData.id] = uiImage
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,8 +147,8 @@ struct ShareView: View {
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder private func imagePreview(_ img: String) -> some View {
|
||||
if let img = UIImage(base64Encoded: img) {
|
||||
@ViewBuilder private func imagePreview(_ imgStr: String) -> some View {
|
||||
if let img = imageFromBase64(imgStr) {
|
||||
previewArea {
|
||||
Image(uiImage: img)
|
||||
.resizable()
|
||||
@@ -163,7 +163,7 @@ struct ShareView: View {
|
||||
@ViewBuilder private func linkPreview(_ linkPreview: LinkPreview) -> some View {
|
||||
previewArea {
|
||||
HStack(alignment: .center, spacing: 8) {
|
||||
if let uiImage = UIImage(base64Encoded: linkPreview.image) {
|
||||
if let uiImage = imageFromBase64(linkPreview.image) {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
|
||||
@@ -194,6 +194,7 @@
|
||||
8CC956EE2BC0041000412A11 /* NetworkObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CC956ED2BC0041000412A11 /* NetworkObserver.swift */; };
|
||||
8CE848A32C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CE848A22C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift */; };
|
||||
B76E6C312C5C41D900EC11AA /* ContactListNavLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = B76E6C302C5C41D900EC11AA /* ContactListNavLink.swift */; };
|
||||
CE176F202C87014C00145DBC /* InvertedForegroundStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE176F1F2C87014C00145DBC /* InvertedForegroundStyle.swift */; };
|
||||
CE1EB0E42C459A660099D896 /* ShareAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE1EB0E32C459A660099D896 /* ShareAPI.swift */; };
|
||||
CE2AD9CE2C452A4D00E844E3 /* ChatUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE2AD9CD2C452A4D00E844E3 /* ChatUtils.swift */; };
|
||||
CE3097FB2C4C0C9F00180898 /* ErrorAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE3097FA2C4C0C9F00180898 /* ErrorAlert.swift */; };
|
||||
@@ -532,6 +533,7 @@
|
||||
8CC956ED2BC0041000412A11 /* NetworkObserver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkObserver.swift; sourceTree = "<group>"; };
|
||||
8CE848A22C5A0FA000D5C7C8 /* SelectableChatItemToolbars.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableChatItemToolbars.swift; sourceTree = "<group>"; };
|
||||
B76E6C302C5C41D900EC11AA /* ContactListNavLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactListNavLink.swift; sourceTree = "<group>"; };
|
||||
CE176F1F2C87014C00145DBC /* InvertedForegroundStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvertedForegroundStyle.swift; sourceTree = "<group>"; };
|
||||
CE1EB0E32C459A660099D896 /* ShareAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareAPI.swift; sourceTree = "<group>"; };
|
||||
CE2AD9CD2C452A4D00E844E3 /* ChatUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatUtils.swift; sourceTree = "<group>"; };
|
||||
CE3097FA2C4C0C9F00180898 /* ErrorAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorAlert.swift; sourceTree = "<group>"; };
|
||||
@@ -789,6 +791,7 @@
|
||||
8C9BC2642C240D5100875A27 /* ThemeModeEditor.swift */,
|
||||
CE984D4A2C36C5D500E3AEFF /* ChatItemClipShape.swift */,
|
||||
CE7548092C622630009579B7 /* SwipeLabel.swift */,
|
||||
CE176F1F2C87014C00145DBC /* InvertedForegroundStyle.swift */,
|
||||
);
|
||||
path = Helpers;
|
||||
sourceTree = "<group>";
|
||||
@@ -1489,6 +1492,7 @@
|
||||
5C9CC7A928C532AB00BEF955 /* DatabaseErrorView.swift in Sources */,
|
||||
5C1A4C1E27A715B700EAD5AD /* ChatItemView.swift in Sources */,
|
||||
64AA1C6927EE10C800AC7277 /* ContextItemView.swift in Sources */,
|
||||
CE176F202C87014C00145DBC /* InvertedForegroundStyle.swift in Sources */,
|
||||
5CEBD7482A5F115D00665FE2 /* SetDeliveryReceiptsView.swift in Sources */,
|
||||
5C9C2DA7289957AE00CC63B1 /* AdvancedNetworkSettings.swift in Sources */,
|
||||
5CADE79A29211BB900072E13 /* PreferencesView.swift in Sources */,
|
||||
|
||||
@@ -2812,13 +2812,14 @@ public enum CIStatus: Decodable, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
public func statusIcon(_ metaColor: Color, _ primaryColor: Color = .accentColor) -> (Image, Color)? {
|
||||
public func statusIcon(_ metaColor: Color, _ paleMetaColor: Color, _ primaryColor: Color = .accentColor) -> (Image, Color)? {
|
||||
switch self {
|
||||
case .sndNew: nil
|
||||
case .sndSent: (Image("checkmark.wide"), metaColor)
|
||||
case let .sndRcvd(msgRcptStatus, _):
|
||||
case let .sndSent(sndProgress):
|
||||
(Image("checkmark.wide"), sndProgress == .partial ? paleMetaColor : metaColor)
|
||||
case let .sndRcvd(msgRcptStatus, sndProgress):
|
||||
switch msgRcptStatus {
|
||||
case .ok: (Image("checkmark.2"), metaColor)
|
||||
case .ok: (Image("checkmark.2"), sndProgress == .partial ? paleMetaColor : metaColor)
|
||||
case .badMsgHash: (Image("checkmark.2"), .red)
|
||||
}
|
||||
case .sndErrorAuth: (Image(systemName: "multiply"), .red)
|
||||
@@ -2830,14 +2831,6 @@ public enum CIStatus: Decodable, Hashable {
|
||||
}
|
||||
}
|
||||
|
||||
public var sndProgress: SndCIStatusProgress? {
|
||||
switch self {
|
||||
case let .sndSent(sndProgress): sndProgress
|
||||
case let .sndRcvd(_ , sndProgress): sndProgress
|
||||
default: nil
|
||||
}
|
||||
}
|
||||
|
||||
public var statusInfo: (String, String)? {
|
||||
switch self {
|
||||
case .sndNew: return nil
|
||||
|
||||
@@ -383,16 +383,30 @@ extension UIImage {
|
||||
}
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
public convenience init?(base64Encoded: String?) {
|
||||
if let base64Encoded, let data = Data(base64Encoded: dropImagePrefix(base64Encoded)) {
|
||||
self.init(data: data)
|
||||
public func imageFromBase64(_ base64Encoded: String?) -> UIImage? {
|
||||
if let base64Encoded {
|
||||
if let img = imageCache.object(forKey: base64Encoded as NSString) {
|
||||
return img
|
||||
} else if let data = Data(base64Encoded: dropImagePrefix(base64Encoded)),
|
||||
let img = UIImage(data: data) {
|
||||
imageCache.setObject(img, forKey: base64Encoded as NSString)
|
||||
return img
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private var imageCache: NSCache<NSString, UIImage> = {
|
||||
var cache = NSCache<NSString, UIImage>()
|
||||
cache.countLimit = 1000
|
||||
return cache
|
||||
}()
|
||||
|
||||
public func getLinkPreview(url: URL, cb: @escaping (LinkPreview?) -> Void) {
|
||||
logger.debug("getLinkMetadata: fetching URL preview")
|
||||
LPMetadataProvider().startFetchingMetadata(for: url){ metadata, error in
|
||||
|
||||
@@ -5,9 +5,8 @@ import android.util.Log
|
||||
import androidx.work.*
|
||||
import chat.simplex.app.SimplexService.Companion.showPassphraseNotification
|
||||
import chat.simplex.common.model.ChatController
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.helpers.DBMigrationResult
|
||||
import chat.simplex.common.platform.chatModel
|
||||
import chat.simplex.common.platform.initChatControllerOnStart
|
||||
import chat.simplex.common.views.helpers.DatabaseUtils
|
||||
import kotlinx.coroutines.*
|
||||
import java.util.Date
|
||||
@@ -30,12 +29,12 @@ object MessagesFetcherWorker {
|
||||
.setConstraints(Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
|
||||
.build()
|
||||
|
||||
WorkManager.getInstance(SimplexApp.context).enqueueUniqueWork(UNIQUE_WORK_TAG, ExistingWorkPolicy.REPLACE, periodicWorkRequest)
|
||||
SimplexApp.context.getWorkManagerInstance().enqueueUniqueWork(UNIQUE_WORK_TAG, ExistingWorkPolicy.REPLACE, periodicWorkRequest)
|
||||
}
|
||||
|
||||
fun cancelAll() {
|
||||
Log.d(TAG, "Worker: canceled all tasks")
|
||||
WorkManager.getInstance(SimplexApp.context).cancelUniqueWork(UNIQUE_WORK_TAG)
|
||||
SimplexApp.context.getWorkManagerInstance().cancelUniqueWork(UNIQUE_WORK_TAG)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import chat.simplex.common.platform.Log
|
||||
import android.content.Intent
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.os.*
|
||||
import android.view.View
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -37,7 +36,7 @@ import java.util.concurrent.TimeUnit
|
||||
|
||||
const val TAG = "SIMPLEX"
|
||||
|
||||
class SimplexApp: Application(), LifecycleEventObserver {
|
||||
class SimplexApp: Application(), LifecycleEventObserver, Configuration.Provider {
|
||||
val chatModel: ChatModel
|
||||
get() = chatController.chatModel
|
||||
|
||||
@@ -164,7 +163,7 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
.addTag(SimplexService.SERVICE_START_WORKER_WORK_NAME_PERIODIC)
|
||||
.build()
|
||||
Log.d(TAG, "ServiceStartWorker: Scheduling period work every ${SimplexService.SERVICE_START_WORKER_INTERVAL_MINUTES} minutes")
|
||||
WorkManager.getInstance(context)?.enqueueUniquePeriodicWork(SimplexService.SERVICE_START_WORKER_WORK_NAME_PERIODIC, workPolicy, work)
|
||||
getWorkManagerInstance().enqueueUniquePeriodicWork(SimplexService.SERVICE_START_WORKER_WORK_NAME_PERIODIC, workPolicy, work)
|
||||
}
|
||||
|
||||
fun schedulePeriodicWakeUp() = CoroutineScope(Dispatchers.Default).launch {
|
||||
@@ -367,4 +366,9 @@ class SimplexApp: Application(), LifecycleEventObserver {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fix for an exception:
|
||||
// WorkManager is not initialized properly. You have explicitly disabled WorkManagerInitializer in your manifest, have not manually called WorkManager#initialize at this point, and your Application does not implement Configuration.Provider.
|
||||
override val workManagerConfiguration: Configuration
|
||||
get() = Configuration.Builder().build()
|
||||
}
|
||||
|
||||
@@ -292,7 +292,7 @@ class SimplexService: Service() {
|
||||
|
||||
fun scheduleStart(context: Context) {
|
||||
Log.d(TAG, "Enqueuing work to start subscriber service")
|
||||
val workManager = WorkManager.getInstance(context)
|
||||
val workManager = context.getWorkManagerInstance()
|
||||
val startServiceRequest = OneTimeWorkRequest.Builder(ServiceStartWorker::class.java).build()
|
||||
workManager.enqueueUniqueWork(WORK_NAME_ONCE, ExistingWorkPolicy.KEEP, startServiceRequest) // Unique avoids races!
|
||||
}
|
||||
|
||||
+15
@@ -6,6 +6,8 @@ import android.net.LocalServerSocket
|
||||
import android.util.Log
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.work.Configuration
|
||||
import androidx.work.WorkManager
|
||||
import java.io.*
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.*
|
||||
@@ -72,3 +74,16 @@ fun initHaskell() {
|
||||
|
||||
initHS()
|
||||
}
|
||||
|
||||
fun Context.getWorkManagerInstance(): WorkManager {
|
||||
// https://github.com/OneSignal/OneSignal-Android-SDK/pull/2052/files
|
||||
// https://github.com/OneSignal/OneSignal-Android-SDK/issues/1672
|
||||
if (!WorkManager.isInitialized()) {
|
||||
try {
|
||||
WorkManager.initialize(this, Configuration.Builder().build())
|
||||
} catch (e: IllegalStateException) {
|
||||
Log.e(TAG, "Error initializing WorkManager: ${e.stackTraceToString()}")
|
||||
}
|
||||
}
|
||||
return WorkManager.getInstance(this)
|
||||
}
|
||||
|
||||
+1
-2
@@ -2,7 +2,6 @@ package chat.simplex.common.views.usersettings
|
||||
|
||||
import SectionView
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.work.WorkManager
|
||||
import chat.simplex.common.model.ChatModel
|
||||
import chat.simplex.common.platform.*
|
||||
import chat.simplex.common.views.helpers.*
|
||||
@@ -33,7 +32,7 @@ fun restartApp() {
|
||||
}
|
||||
|
||||
private fun shutdownApp() {
|
||||
WorkManager.getInstance(androidAppContext).cancelAllWork()
|
||||
androidAppContext.getWorkManagerInstance().cancelAllWork()
|
||||
platform.androidServiceSafeStop()
|
||||
Runtime.getRuntime().exit(0)
|
||||
}
|
||||
|
||||
+1
-1
@@ -186,7 +186,7 @@ fun ErrorChatListItem() {
|
||||
|
||||
suspend fun directChatAction(rhId: Long?, contact: Contact, chatModel: ChatModel) {
|
||||
when {
|
||||
contact.activeConn == null && contact.profile.contactLink != null -> askCurrentOrIncognitoProfileConnectContactViaAddress(chatModel, rhId, contact, close = null, openChat = true)
|
||||
contact.activeConn == null && contact.profile.contactLink != null && contact.active -> askCurrentOrIncognitoProfileConnectContactViaAddress(chatModel, rhId, contact, close = null, openChat = true)
|
||||
else -> openChat(rhId, ChatInfo.Direct(contact), chatModel)
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -99,7 +99,7 @@ fun chatContactType(chat: Chat): ContactType {
|
||||
val contact = cInfo.contact
|
||||
|
||||
when {
|
||||
contact.activeConn == null && contact.profile.contactLink != null -> ContactType.CARD
|
||||
contact.activeConn == null && contact.profile.contactLink != null && contact.active -> ContactType.CARD
|
||||
contact.chatDeleted -> ContactType.CHAT_DELETED
|
||||
contact.contactStatus == ContactStatus.Active -> ContactType.RECENT
|
||||
else -> ContactType.UNLISTED
|
||||
|
||||
Reference in New Issue
Block a user