mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-07-28 16:29:49 +00:00
ios: blur for media (#4512)
* ios: blur for media * line * one more place * changes for video * using notification center * change * unused code * string * simplify * refactor ifs * fix --------- Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
This commit is contained in:
co-authored by
Evgeny Poberezkin
parent
70d577260b
commit
a966f6b19d
@@ -17,6 +17,7 @@ struct CIImageView: View {
|
||||
var imgWidth: CGFloat?
|
||||
var smallView: Bool = false
|
||||
@Binding var showFullScreenImage: Bool
|
||||
@State private var blurred: Bool = UserDefaults.standard.integer(forKey: DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS) > 0
|
||||
|
||||
var body: some View {
|
||||
let file = chatItem.file
|
||||
@@ -26,12 +27,21 @@ struct CIImageView: View {
|
||||
.fullScreenCover(isPresented: $showFullScreenImage) {
|
||||
FullScreenMediaView(chatItem: chatItem, image: uiImage, showView: $showFullScreenImage)
|
||||
}
|
||||
.if(!smallView) { view in
|
||||
view.modifier(PrivacyBlur(blurred: $blurred))
|
||||
}
|
||||
.onTapGesture { showFullScreenImage = true }
|
||||
.onChange(of: m.activeCallViewIsCollapsed) { _ in
|
||||
showFullScreenImage = false
|
||||
}
|
||||
} else if let preview {
|
||||
Group { if smallView { smallViewImageView(preview) } else { imageView(preview) } }
|
||||
Group {
|
||||
if smallView {
|
||||
smallViewImageView(preview)
|
||||
} else {
|
||||
imageView(preview).modifier(PrivacyBlur(blurred: $blurred))
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
if let file = file {
|
||||
switch file.fileStatus {
|
||||
@@ -102,7 +112,9 @@ struct CIImageView: View {
|
||||
.frame(width: w, height: w * img.size.height / img.size.width)
|
||||
.scaledToFit()
|
||||
}
|
||||
loadingIndicator()
|
||||
if !blurred || !showDownloadButton(chatItem.file?.fileStatus) {
|
||||
loadingIndicator()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,4 +178,12 @@ struct CIImageView: View {
|
||||
.tint(.white)
|
||||
.padding(8)
|
||||
}
|
||||
|
||||
private func showDownloadButton(_ fileStatus: CIFileStatus?) -> Bool {
|
||||
switch fileStatus {
|
||||
case .rcvInvitation: true
|
||||
case .rcvAborted: true
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import SimpleXChat
|
||||
struct CILinkView: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
let linkPreview: LinkPreview
|
||||
@State private var blurred: Bool = UserDefaults.standard.integer(forKey: DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS) > 0
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .center, spacing: 6) {
|
||||
@@ -19,6 +20,7 @@ struct CILinkView: View {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.modifier(PrivacyBlur(blurred: $blurred))
|
||||
}
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(linkPreview.title)
|
||||
|
||||
@@ -31,6 +31,7 @@ struct CIVideoView: View {
|
||||
@State private var fullScreenTimeObserver: Any? = nil
|
||||
@State private var publisher: AnyCancellable? = nil
|
||||
private var sizeMultiplier: CGFloat { smallView ? 0.38 : 1 }
|
||||
@State private var blurred: Bool = UserDefaults.standard.integer(forKey: DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS) > 0
|
||||
|
||||
init(chatItem: ChatItem, preview: UIImage?, duration: Int, maxWidth: CGFloat, videoWidth: CGFloat?, smallView: Bool = false, showFullscreenPlayer: Binding<Bool>) {
|
||||
self.chatItem = chatItem
|
||||
@@ -85,17 +86,19 @@ struct CIVideoView: View {
|
||||
durationProgress()
|
||||
}
|
||||
}
|
||||
if !smallView, let file = file, showDownloadButton(file.fileStatus) {
|
||||
Button {
|
||||
receiveFileIfValidSize(file: file, receiveFile: receiveFile)
|
||||
} label: {
|
||||
playPauseIcon("play.fill")
|
||||
}
|
||||
} else if let file = file, showDownloadButton(file.fileStatus) && !file.showStatusIconInSmallView {
|
||||
playPauseIcon("play.fill")
|
||||
.onTapGesture {
|
||||
if !blurred, let file, showDownloadButton(file.fileStatus) {
|
||||
if !smallView {
|
||||
Button {
|
||||
receiveFileIfValidSize(file: file, receiveFile: receiveFile)
|
||||
} label: {
|
||||
playPauseIcon("play.fill")
|
||||
}
|
||||
} else if !file.showStatusIconInSmallView {
|
||||
playPauseIcon("play.fill")
|
||||
.onTapGesture {
|
||||
receiveFileIfValidSize(file: file, receiveFile: receiveFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.fullScreenCover(isPresented: $showFullScreenPlayer) {
|
||||
@@ -135,7 +138,7 @@ struct CIVideoView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func showDownloadButton(_ fileStatus: CIFileStatus) -> Bool {
|
||||
private func showDownloadButton(_ fileStatus: CIFileStatus?) -> Bool {
|
||||
switch fileStatus {
|
||||
case .rcvInvitation: true
|
||||
case .rcvAborted: true
|
||||
@@ -156,20 +159,22 @@ struct CIVideoView: View {
|
||||
.onChange(of: m.activeCallViewIsCollapsed) { _ in
|
||||
showFullScreenPlayer = false
|
||||
}
|
||||
if !decryptionInProgress {
|
||||
Button {
|
||||
decrypt(file: file) {
|
||||
if urlDecrypted != nil {
|
||||
videoPlaying = true
|
||||
player?.play()
|
||||
if !blurred {
|
||||
if !decryptionInProgress {
|
||||
Button {
|
||||
decrypt(file: file) {
|
||||
if urlDecrypted != nil {
|
||||
videoPlaying = true
|
||||
player?.play()
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
playPauseIcon(canBePlayed ? "play.fill" : "play.slash")
|
||||
}
|
||||
} label: {
|
||||
playPauseIcon(canBePlayed ? "play.fill" : "play.slash")
|
||||
.disabled(!canBePlayed)
|
||||
} else {
|
||||
videoDecryptionProgress()
|
||||
}
|
||||
.disabled(!canBePlayed)
|
||||
} else {
|
||||
videoDecryptionProgress()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -188,6 +193,7 @@ struct CIVideoView: View {
|
||||
videoPlaying = false
|
||||
}
|
||||
}
|
||||
.modifier(PrivacyBlur(enabled: !videoPlaying, blurred: $blurred))
|
||||
.onTapGesture {
|
||||
switch player.timeControlStatus {
|
||||
case .playing:
|
||||
@@ -203,7 +209,7 @@ struct CIVideoView: View {
|
||||
.onChange(of: m.activeCallViewIsCollapsed) { _ in
|
||||
showFullScreenPlayer = false
|
||||
}
|
||||
if !videoPlaying {
|
||||
if !videoPlaying && !blurred {
|
||||
Button {
|
||||
m.stopPreviousRecPlay = url
|
||||
player.play()
|
||||
@@ -317,7 +323,10 @@ struct CIVideoView: View {
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: w)
|
||||
fileStatusIcon()
|
||||
.modifier(PrivacyBlur(blurred: $blurred))
|
||||
if !blurred || !showDownloadButton(chatItem.file?.fileStatus) {
|
||||
fileStatusIcon()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -129,8 +129,9 @@ struct ReverseList<Item: Identifiable & Hashable & Sendable, Content: View>: UIV
|
||||
from: nil,
|
||||
for: nil
|
||||
)
|
||||
NotificationCenter.default.post(name: .chatViewWillBeginScrolling, object: nil)
|
||||
}
|
||||
|
||||
|
||||
/// Scrolls up
|
||||
func scrollToNextPage() {
|
||||
tableView.setContentOffset(
|
||||
|
||||
@@ -17,3 +17,37 @@ extension View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Notification.Name {
|
||||
static let chatViewWillBeginScrolling = Notification.Name("chatWillBeginScrolling")
|
||||
}
|
||||
|
||||
struct PrivacyBlur: ViewModifier {
|
||||
var enabled: Bool = true
|
||||
@Binding var blurred: Bool
|
||||
@AppStorage(DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS) private var blurRadius: Int = 0
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
if blurRadius > 0 {
|
||||
// parallel ifs are necessary here because otherwise some views flicker,
|
||||
// e.g. when playing video
|
||||
content
|
||||
.blur(radius: blurred && enabled ? CGFloat(blurRadius) * 0.5 : 0)
|
||||
.overlay {
|
||||
if (blurred && enabled) {
|
||||
Color.clear.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
blurred = false
|
||||
}
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .chatViewWillBeginScrolling)) { _ in
|
||||
if !blurred {
|
||||
blurred = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ extension AppSettings {
|
||||
if let val = privacyShowChatPreviews { def.setValue(val, forKey: DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS) }
|
||||
if let val = privacySaveLastDraft { def.setValue(val, forKey: DEFAULT_PRIVACY_SAVE_LAST_DRAFT) }
|
||||
if let val = privacyProtectScreen { def.setValue(val, forKey: DEFAULT_PRIVACY_PROTECT_SCREEN) }
|
||||
if let val = privacyMediaBlurRadius { def.setValue(val, forKey: DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS) }
|
||||
if let val = notificationMode { ChatModel.shared.notificationMode = val.toNotificationsMode() }
|
||||
if let val = notificationPreviewMode { ntfPreviewModeGroupDefault.set(val) }
|
||||
if let val = webrtcPolicyRelay { def.setValue(val, forKey: DEFAULT_WEBRTC_POLICY_RELAY) }
|
||||
@@ -62,6 +63,7 @@ extension AppSettings {
|
||||
c.privacyShowChatPreviews = def.bool(forKey: DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS)
|
||||
c.privacySaveLastDraft = def.bool(forKey: DEFAULT_PRIVACY_SAVE_LAST_DRAFT)
|
||||
c.privacyProtectScreen = def.bool(forKey: DEFAULT_PRIVACY_PROTECT_SCREEN)
|
||||
c.privacyMediaBlurRadius = def.integer(forKey: DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS)
|
||||
c.notificationMode = AppSettingsNotificationMode.from(ChatModel.shared.notificationMode)
|
||||
c.notificationPreviewMode = ntfPreviewModeGroupDefault.get()
|
||||
c.webrtcPolicyRelay = def.bool(forKey: DEFAULT_WEBRTC_POLICY_RELAY)
|
||||
|
||||
@@ -22,6 +22,7 @@ struct PrivacySettings: View {
|
||||
@AppStorage(DEFAULT_PRIVACY_PROTECT_SCREEN) private var protectScreen = false
|
||||
@AppStorage(DEFAULT_PERFORM_LA) private var prefPerformLA = false
|
||||
@State private var currentLAMode = privacyLocalAuthModeDefault.get()
|
||||
@AppStorage(DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS) private var privacyMediaBlurRadius: Int = 0
|
||||
@State private var contactReceipts = false
|
||||
@State private var contactReceiptsReset = false
|
||||
@State private var contactReceiptsOverrides = 0
|
||||
@@ -113,6 +114,22 @@ struct PrivacySettings: View {
|
||||
privacyAcceptImagesGroupDefault.set($0)
|
||||
}
|
||||
}
|
||||
settingsRow("circle.rectangle.filled.pattern.diagonalline", color: theme.colors.secondary) {
|
||||
Picker("Blur media", selection: $privacyMediaBlurRadius) {
|
||||
let values = [0, 12, 24, 48] + ([0, 12, 24, 48].contains(privacyMediaBlurRadius) ? [] : [privacyMediaBlurRadius])
|
||||
ForEach(values, id: \.self) { radius in
|
||||
let text: String = switch radius {
|
||||
case 0: NSLocalizedString("Off", comment: "blur media")
|
||||
case 12: NSLocalizedString("Soft", comment: "blur media")
|
||||
case 24: NSLocalizedString("Medium", comment: "blur media")
|
||||
case 48: NSLocalizedString("Strong", comment: "blur media")
|
||||
default: "\(radius)"
|
||||
}
|
||||
Text(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(height: 36)
|
||||
settingsRow("network.badge.shield.half.filled", color: theme.colors.secondary) {
|
||||
Toggle("Protect IP address", isOn: $askToApproveRelays)
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ let DEFAULT_PRIVACY_SHOW_CHAT_PREVIEWS = "privacyShowChatPreviews"
|
||||
let DEFAULT_PRIVACY_SAVE_LAST_DRAFT = "privacySaveLastDraft"
|
||||
let DEFAULT_PRIVACY_PROTECT_SCREEN = "privacyProtectScreen"
|
||||
let DEFAULT_PRIVACY_DELIVERY_RECEIPTS_SET = "privacyDeliveryReceiptsSet"
|
||||
let DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS = "privacyMediaBlurRadius"
|
||||
let DEFAULT_EXPERIMENTAL_CALLS = "experimentalCalls"
|
||||
let DEFAULT_CHAT_ARCHIVE_NAME = "chatArchiveName"
|
||||
let DEFAULT_CHAT_ARCHIVE_TIME = "chatArchiveTime"
|
||||
@@ -87,6 +88,7 @@ let appDefaults: [String: Any] = [
|
||||
DEFAULT_PRIVACY_SAVE_LAST_DRAFT: true,
|
||||
DEFAULT_PRIVACY_PROTECT_SCREEN: false,
|
||||
DEFAULT_PRIVACY_DELIVERY_RECEIPTS_SET: false,
|
||||
DEFAULT_PRIVACY_MEDIA_BLUR_RADIUS: 0,
|
||||
DEFAULT_EXPERIMENTAL_CALLS: false,
|
||||
DEFAULT_CHAT_V3_DB_MIGRATION: V3DBMigrationState.offer.rawValue,
|
||||
DEFAULT_DEVELOPER_TOOLS: false,
|
||||
|
||||
@@ -2096,6 +2096,7 @@ public struct AppSettings: Codable, Equatable, Hashable {
|
||||
public var privacyShowChatPreviews: Bool? = nil
|
||||
public var privacySaveLastDraft: Bool? = nil
|
||||
public var privacyProtectScreen: Bool? = nil
|
||||
public var privacyMediaBlurRadius: Int? = nil
|
||||
public var notificationMode: AppSettingsNotificationMode? = nil
|
||||
public var notificationPreviewMode: NotificationPreviewMode? = nil
|
||||
public var webrtcPolicyRelay: Bool? = nil
|
||||
@@ -2125,6 +2126,7 @@ public struct AppSettings: Codable, Equatable, Hashable {
|
||||
if privacyShowChatPreviews != def.privacyShowChatPreviews { empty.privacyShowChatPreviews = privacyShowChatPreviews }
|
||||
if privacySaveLastDraft != def.privacySaveLastDraft { empty.privacySaveLastDraft = privacySaveLastDraft }
|
||||
if privacyProtectScreen != def.privacyProtectScreen { empty.privacyProtectScreen = privacyProtectScreen }
|
||||
if privacyMediaBlurRadius != def.privacyMediaBlurRadius { empty.privacyMediaBlurRadius = privacyMediaBlurRadius }
|
||||
if notificationMode != def.notificationMode { empty.notificationMode = notificationMode }
|
||||
if notificationPreviewMode != def.notificationPreviewMode { empty.notificationPreviewMode = notificationPreviewMode }
|
||||
if webrtcPolicyRelay != def.webrtcPolicyRelay { empty.webrtcPolicyRelay = webrtcPolicyRelay }
|
||||
@@ -2155,6 +2157,7 @@ public struct AppSettings: Codable, Equatable, Hashable {
|
||||
privacyShowChatPreviews: true,
|
||||
privacySaveLastDraft: true,
|
||||
privacyProtectScreen: false,
|
||||
privacyMediaBlurRadius: 0,
|
||||
notificationMode: AppSettingsNotificationMode.instant,
|
||||
notificationPreviewMode: NotificationPreviewMode.message,
|
||||
webrtcPolicyRelay: true,
|
||||
|
||||
Reference in New Issue
Block a user