copy monal src

This commit is contained in:
Woit 2024-11-18 15:53:52 +01:00
parent 8fbf7c5105
commit 10475f9a72
543 changed files with 88554 additions and 116 deletions

173
.gitignore vendored
View file

@ -1,123 +1,74 @@
docs
Frameworks
######################### # rust bridge
# **.gitignore** file for Xcode4 / OS X Source projects rust/monal-rust-swift-bridge/generated
# rust/LibMonalRustSwiftBridge
# NB: if you are storing "built" products, this WILL NOT WORK,
# and you should use a different **.gitignore** (or none at all)
# This file is for SOURCE projects, where there are many extra
# files that we want to exclude
#
# For updates, see: http://stackoverflow.com/questions/49478/git-ignore-file-for-xcode-projects
#########################
##### # Created by https://www.toptal.com/developers/gitignore/api/rust
# OS X temporary files that should never be committed # Edit at https://www.toptal.com/developers/gitignore?templates=rust
### Rust ###
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
!rust/Cargo.lock
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
# End of https://www.toptal.com/developers/gitignore/api/rust
# macOS
.DS_Store .DS_Store
*.swp
profile
# Xcode
Monal/Monal.xcodeproj/xcuserdata/*
Monal/Monal.xcodeproj/project.xcworkspace/xcshareddata/*
Monal/Monal.xcodeproj/project.xcworkspace/xcuserdata/*
contents.xcworkspacedata
._*
*.xcscmblueprint
*.xccheckout
Monal/Monal.xcworkspace/xcshareddata/*
Monal/Monal.xcworkspace/xcuserdata/*
#### Monal/build/*
# Xcode temporary files that should never be committed
#
# NB: NIB/XIB files still exist even on Storyboard projects, so we want this...
*~.nib
####
# Xcode build files -
#
# NB: slash on the end, so we only remove the FOLDER, not any files that were badly named "DerivedData"
DerivedData/
# NB: slash on the end, so we only remove the FOLDER, not any files that were badly named "build"
build/
#####
# Xcode private settings (window sizes, bookmarks, breakpoints, custom executables, smart groups)
#
# This is complicated:
#
# SOMETIMES you need to put this file in version control.
# Apple designed it poorly - if you use "custom executables", they are
# saved in this file.
# 99% of projects do NOT use those, so they do NOT want to version control this file.
# ..but if you're in the 1%, comment out the line "*.pbxuser"
*.pbxuser *.pbxuser
*.mode1v3
*.mode2v3
*.perspectivev3
# NB: also, whitelist the default ones, some projects need to use these
!default.pbxuser !default.pbxuser
*.mode1v3
!default.mode1v3 !default.mode1v3
*.mode2v3
!default.mode2v3 !default.mode2v3
*.perspectivev3
!default.perspectivev3 !default.perspectivev3
profile
####
# Xcode 4 - semi-personal settings, often included in workspaces
#
# You can safely ignore the xcuserdata files - but do NOT ignore the files next to them
#
xcuserdata
####
# XCode 4 workspaces - more detailed
#
# Workspaces are important! They are a core feature of Xcode - don't exclude them :)
#
# Workspace layout is quite spammy. For reference:
#
# (root)/
# (project-name).xcodeproj/
# project.pbxproj
# project.xcworkspace/
# contents.xcworkspacedata
# xcuserdata/
# (your name)/xcuserdatad/
# xcuserdata/
# (your name)/xcuserdatad/
#
#
#
# Xcode 4 workspaces - SHARED
#
# This is UNDOCUMENTED (google: "developer.apple.com xcshareddata" - 0 results
# But if you're going to kill personal workspaces, at least keep the shared ones...
#
#
!xcshareddata
####
# XCode 4 build-schemes
#
# PRIVATE ones are stored inside xcuserdata
!xcschemes
####
# Xcode 4 - Deprecated classes
#
# Allegedly, if you manually "deprecate" your classes, they get moved here.
#
# We're using source-control, so this is a "feature" that we do not want!
*.moved-aside *.moved-aside
/.idea DerivedData
/ConversationsClassic/.idea Monal/.nvim
/ConversationsClassic.xcodeproj Monal/buildServer.json
/Info.plist
/ConversationsClassic/ConversationsClassic.entitlements # Pods
/XMPPSwift/Client/VoIP/rickroll.mp4 Monal/Pods
/.nvim
/buildServer.json #Don't accidentally commit localization state
TODO.txt Monal/localization/external
PASSWD.txt Monal/localization/external/*
Monal/shareSheet-iOS/localization/external
Monal/shareSheet-iOS/localization/external/*
# certs and other encrypted stuff
*.pem
*.key
*.csr
*.cer
*.mobileprovision
*.provisionprofile
*.p12
Monal/Classes/secrets.h

BIN
Art/alpha_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

BIN
Art/callkit_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

BIN
Art/chat.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

BIN
Art/chat2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

BIN
Art/chat_dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

BIN
Art/friends.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

BIN
Art/friends2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

BIN
Art/friends_dark.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

BIN
Art/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 KiB

149
Art/monal.svg Normal file
View file

@ -0,0 +1,149 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="20mm"
height="20.085215mm"
viewBox="0 0 20 20.085215"
version="1.1"
id="svg3834"
inkscape:version="0.92.2 5c3e80d, 2017-08-06"
sodipodi:docname="monal.svg"
style="enable-background:new"
inkscape:export-filename="/Users/anurodhp/Desktop/monal.png"
inkscape:export-xdpi="1300.48"
inkscape:export-ydpi="1300.48">
<defs
id="defs3828">
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 10.042608 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="20 : 10.042608 : 1"
inkscape:persp3d-origin="10 : 6.6950717 : 1"
id="perspective841" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="9.0725624"
inkscape:cx="28.787632"
inkscape:cy="44.279616"
inkscape:document-units="mm"
inkscape:current-layer="layer3"
showgrid="false"
inkscape:window-width="1280"
inkscape:window-height="700"
inkscape:window-x="-4"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:pagecheckerboard="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0" />
<metadata
id="metadata3831">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-181.15717,-221.0978)"
style="display:inline">
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="Layer 2"
style="display:inline">
<rect
id="rect30"
width="20"
height="20"
x="181.15717"
y="221.18301"
style="stroke-width:0.26458332" />
<rect
style="fill:#2cd3e3;fill-opacity:1;stroke-width:0.26458332;opacity:1"
id="rect48"
width="20"
height="20"
x="181.15717"
y="221.18301"
inkscape:export-xdpi="1299.6801"
inkscape:export-ydpi="1299.6801" />
</g>
<g
id="g25"
transform="matrix(1.25,0,0,1.25,-45.477532,-55.597438)">
<g
id="g4578"
transform="translate(0.8477441,0.34537723)">
<path
style="fill:#ffffff;fill-opacity:1;stroke-width:0.21166666"
d="m 191.56803,221.43167 c 0.44531,-0.54927 1.00283,-1.55943 1.42657,-1.47418 -1.6117,3.28945 -5.09363,5.55983 -6.77643,8.41276 -1.48053,1.02259 -5.59221,4.97552 -3.50788,4.62647 2.55426,-0.33289 4.26942,-0.18434 6.47037,-0.0863 2.97857,0.44913 2.99008,0.35922 1.42237,-0.94939 -4.08407,-2.92741 -7.3886,-5.96422 -2.55345,-2.51434 1.19608,0.89899 3.58977,2.88011 4.62961,3.96256 0.14192,0.38081 -1.36875,-1.03083 -1.63269,-1.38151 1.00787,0.47021 4.41857,3.94275 5.65285,3.45204 1.11708,-0.36357 1.5434,-1.4884 2.64165,-0.91497 0.73976,-0.52896 -0.81203,-1.84714 0.109,-2.45498 1.18118,-0.18435 -0.2902,-1.51785 0.93686,-1.87197 0.72674,-0.62762 -1.09372,-1.01894 -1.52077,-1.45287 -0.89602,-0.45402 -0.35512,-1.55824 -0.004,-1.79086 0.0846,-1.03756 1.78245,-0.86434 2.36976,-1.66716 0.74024,-0.20999 1.24713,-1.13045 0.10892,-1.05465 -1.29104,-0.33585 -2.48308,0.3858 -3.65095,0.80824 -0.75744,0.43619 -1.51475,-0.3745 -0.67602,-0.9122 0.086,-0.87983 -0.49258,-4.71845 2.78132,-3.06301 0.34122,0.76448 -1.29924,0.73222 -1.56848,1.44997 -0.51725,0.58889 -0.82268,1.36738 -0.81609,2.1534 1.37222,-0.61392 1.59347,-2.39848 2.89681,-3.09724 0.62326,-0.9785 1.02124,0.86222 0.0967,0.71688 -0.41512,0.46396 -1.82667,1.32778 -1.43703,1.64655 0.99965,-0.74709 2.16479,-0.20775 3.17351,0.0358 1.13327,0.97208 -0.7695,1.66177 -1.40739,2.19644 -0.72103,0.3951 -1.48929,0.73257 -1.53088,1.60897 -0.81354,-0.32254 0.12422,1.14184 0.65017,1.27543 0.43315,0.20734 0.91104,0.44698 0.50741,-0.12798 0.30174,-0.53571 1.10164,0.28036 0.4594,-0.4453 0.0479,-1.20018 1.96828,-0.76448 2.64659,-1.55994 1.00428,-0.71951 -0.0847,-1.22662 -0.85018,-1.0046 -0.50182,0.06 -1.07873,-0.43882 -0.22366,-0.39397 0.83328,-0.14122 2.91477,0.25377 1.94632,1.4131 -1.06336,0.52011 -1.39764,1.49535 -1.68824,2.5366 -0.64436,0.88909 0.0567,1.65993 0.51433,2.21278 -0.0755,1.02463 -0.9696,1.82839 -1.60005,2.58943 -0.34214,0.34256 -1.17179,0.99352 -0.4029,0.26644 0.9274,-1.03713 1.17061,-2.97085 -0.091,-3.84667 -0.84429,-0.40075 -0.98573,0.46872 -0.61694,1.00935 -0.25867,0.8 -0.87545,1.54176 -0.22698,2.35526 -0.13692,1.16852 -1.3052,0.0978 -1.98479,0.71485 -0.94554,0.26803 -0.56905,0.54435 -1.5424,0.66888 1.68588,0.55388 2.10672,-0.0925 3.78947,-0.34726 -0.64558,0.75646 -1.80506,1.10898 -2.81213,1.13009 -2.03076,-0.002 -3.80891,-1.10663 -5.61502,-1.89118 -1.12924,-0.54753 -4.82644,-1.3203 -6.13091,-1.1211 -1.74005,0.0829 -1.83717,0.0547 -3.444,0.29301 -1.26438,-0.004 -1.42104,-0.75538 -0.51486,-2.02424 3.57949,-3.11534 6.69133,-7.03177 9.59613,-10.08739 z m -1.14004,8.46391 c -6.64532,4.78894 -3.32266,2.39447 0,0 z m -1.20322,0.0301 c -1.1412,-0.63874 -0.31307,-2.07894 0.17279,-2.89173 0.89169,-1.36972 2.86985,-1.39236 4.16156,-0.61159 1.54938,1.0298 2.07366,1.03412 2.06974,1.79612 -1.4368,0.34261 -2.55154,1.41885 -3.96896,1.80186 -0.79909,0.16878 -1.64355,0.0662 -2.43562,-0.0947 z m 3.57167,-0.77942 c 1.35312,-0.0504 0.6421,-1.87946 -0.27756,-2.06232 -2.33109,-0.63103 -2.8492,0.97005 -3.34013,1.46545 0.0455,1.37625 2.35919,1.3962 3.29693,0.77529 0.10657,-0.0601 -0.10797,0.0576 0,0 z m -3.61769,-0.59687 c -5.81249,5.68643 -2.90624,2.84321 0,0 z m 4.87351,-8.29404 c -9.0615,11.21579 -4.53075,5.60789 0,0 z"
id="path3882"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" />
</g>
</g>
<ellipse
style="fill:#ffffff;fill-opacity:1;stroke-width:0.26458332"
id="path4583"
cx="185.16042"
cy="234.62871"
rx="0.74570084"
ry="0.51021636" />
</g>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="Layer 3"
style="display:inline">
<path
style="opacity:1;vector-effect:none;fill:#2cd3e3;fill-opacity:1;fill-rule:evenodd;stroke-width:0.43717846;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="path884"
sodipodi:type="arc"
sodipodi:cx="7.5222836"
sodipodi:cy="13.490612"
sodipodi:rx="3.1455584"
sodipodi:ry="1.9101746"
sodipodi:start="0"
sodipodi:end="6.1045858"
sodipodi:open="true"
d="M 10.667842,13.490612 A 3.1455584,1.9101746 0 0 1 7.6626857,15.398883 3.1455584,1.9101746 0 0 1 4.3892589,13.660964 3.1455584,1.9101746 0 0 1 7.102196,11.597548 3.1455584,1.9101746 0 0 1 10.617807,13.151267"
transform="matrix(0.93916761,-0.34345916,0.51106856,0.85953995,0,0)" />
</g>
<g
inkscape:groupmode="layer"
id="layer4"
inkscape:label="Layer 4"
style="display:inline">
<circle
style="opacity:1;vector-effect:none;fill:#b3ff80;fill-opacity:1;fill-rule:evenodd;stroke-width:0.38550848;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="path907"
cx="13.877442"
cy="8.9755268"
r="1.5" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.9 KiB

BIN
Art/park_black_white.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 KiB

BIN
Art/park_colors.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 477 KiB

BIN
Art/park_white_black.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 623 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 617 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

29
LICENSE Normal file
View file

@ -0,0 +1,29 @@
BSD 2-Clause License
Copyright (c) 2024, Thilo Molitor
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
The views and conclusions contained in the software and documentation are those
of the authors and should not be interpreted as representing official policies,
either expressed or implied, of Anurodh Pokharel

27
Monal/.bartycrouch.toml Normal file
View file

@ -0,0 +1,27 @@
[update]
tasks = ["interfaces", "code", "normalize"]
[update.interfaces]
paths = ["."]
defaultToBase = true
ignoreEmptyStrings = true
unstripped = false
[update.code]
codePaths = ["Classes", "shareSheet-iOS", "NotificationService"]
localizablePaths = ["localization", "shareSheet-iOS/localization"]
defaultToKeys = true
additive = true
unstripped = false
plistArguments = true
[update.normalize]
paths = ["."]
sourceLocale = "base"
harmonizeWithSource = true
sortByKeys = true
[lint]
paths = ["."]
duplicateKeys = true
emptyValues = true

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>group.monalalpha</string>
</array>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>

BIN
Monal/CallSounds/busy.wav Normal file

Binary file not shown.

BIN
Monal/CallSounds/error.wav Normal file

Binary file not shown.

Binary file not shown.

26
Monal/Classes/AESGcm.h Normal file
View file

@ -0,0 +1,26 @@
//
// AESGcm.h
// Monal
//
// Created by Anurodh Pokharel on 4/19/19.
// Copyright © 2019 Monal.im. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "MLEncryptedPayload.h"
NS_ASSUME_NONNULL_BEGIN
@interface AESGcm : NSObject
/**
key size should be 16 or 32
*/
+(MLEncryptedPayload* _Nullable) encrypt:(NSData*) body keySize:(int) keySize;
+(MLEncryptedPayload* _Nullable) encrypt:(NSData*) body withKey:(NSData*) gcmKey;
+(NSData* _Nullable) decrypt:(NSData *)body withKey:(NSData *) key andIv:(NSData *)iv withAuth:(NSData * _Nullable) auth;
+(NSData* _Nullable) genIV;
+(NSData* _Nullable) genKey:(int) keySize;
@end
NS_ASSUME_NONNULL_END

71
Monal/Classes/AESGcm.m Normal file
View file

@ -0,0 +1,71 @@
//
// AESGcm.m
// Monal
//
// Created by Anurodh Pokharel on 4/19/19.
// Copyright © 2019 Monal.im. All rights reserved.
//
#import "MLConstants.h"
#import "AESGcm.h"
#import <monalxmpp/monalxmpp-Swift.h>
@class MLCrypto;
@implementation AESGcm
+(MLEncryptedPayload*) encrypt:(NSData*) body keySize:(int) keySize
{
NSData* gcmKey = [self genKey:keySize];
if(!gcmKey)
{
return nil;
}
return [self encrypt:body withKey:gcmKey];
}
+(MLEncryptedPayload*) encrypt:(NSData*) body withKey:(NSData*) gcmKey
{
MLCrypto* crypto = [MLCrypto new];
EncryptedPayload* payload = [crypto encryptGCMWithKey:gcmKey decryptedContent:body];
if(payload == nil)
{
return nil;
}
NSMutableData* combinedKey = [NSMutableData dataWithData:gcmKey];
[combinedKey appendData:payload.tag];
if(combinedKey == nil)
{
return nil;
}
return [[MLEncryptedPayload alloc] initWithBody:payload.body key:combinedKey iv:payload.iv authTag:payload.tag];
}
+(NSData*) genIV
{
MLCrypto* crypto = [MLCrypto new];
return [crypto genIV];
}
+(NSData*) genKey:(int) keySize
{
uint8_t randomBytes[keySize];
if(SecRandomCopyBytes(kSecRandomDefault, keySize, randomBytes) != 0)
return nil;
return [[NSData alloc] initWithBytes:randomBytes length:keySize];
}
+(NSData*) decrypt:(NSData*) body withKey:(NSData*) key andIv:(NSData*) iv withAuth:(NSData* _Nullable) auth
{
MLCrypto* crypto = [MLCrypto new];
NSMutableData* combined = [NSMutableData new];
[combined appendData:iv];
[combined appendData:body];
[combined appendData:auth]; //if auth is nil assume it already was apended to body
NSData* toReturn = [crypto decryptGCMWithKey:key encryptedContent:combined];
return toReturn;
}
@end

View file

@ -0,0 +1,594 @@
//
// AVCallUI.swift
// Monal
//
// Created by Thilo Molitor on 20.12.22.
// Copyright © 2021 Monal.im. All rights reserved.
//
import WebRTC
import AVFoundation
import CallKit
import AVKit
struct VideoView: UIViewRepresentable {
var renderer: RTCMTLVideoView
init(renderer: RTCMTLVideoView) {
self.renderer = renderer
}
func makeUIView(context: Context) -> RTCMTLVideoView {
return self.renderer
}
func updateUIView(_ renderer: RTCMTLVideoView, context: Context) {
DDLogDebug("updateUIView called...")
//do nothing
}
}
struct AVCallUI: View {
@StateObject private var appDelegate: ObservableKVOWrapper<MonalAppDelegate>
@StateObject private var call: ObservableKVOWrapper<MLCall>
@StateObject private var contact: ObservableKVOWrapper<MLContact>
@State private var showMicAlert = false
@State private var showSecurityHelpAlert: MLCallEncryptionState? = nil
@State private var controlsVisible = true
@State private var localRendererLocation: CGPoint = CGPoint(
x: UIScreen.main.bounds.size.width - (UIScreen.main.bounds.size.width/5.0/2.0 + 24.0),
y: UIScreen.main.bounds.size.height/5.0/2.0 + 16.0
)
@State private var cameraPosition: AVCaptureDevice.Position = .front
@State private var sendingVideo = true
private var ringingPlayer: AVAudioPlayer!
private var busyPlayer: AVAudioPlayer!
private var errorPlayer: AVAudioPlayer!
private var delegate: SheetDismisserProtocol
private var formatter: DateComponentsFormatter
private var localRenderer: RTCMTLVideoView
private var remoteRenderer: RTCMTLVideoView
init(delegate: SheetDismisserProtocol, call: MLCall) {
_call = StateObject(wrappedValue: ObservableKVOWrapper(call))
_contact = StateObject(wrappedValue: ObservableKVOWrapper(call.contact))
_appDelegate = StateObject(wrappedValue: ObservableKVOWrapper(UIApplication.shared.delegate as! MonalAppDelegate))
self.delegate = delegate
self.formatter = DateComponentsFormatter()
self.formatter.allowedUnits = [.hour, .minute, .second]
self.formatter.unitsStyle = .positional
self.formatter.zeroFormattingBehavior = .pad
//use the complete screen for remote video
self.remoteRenderer = RTCMTLVideoView(frame: UIScreen.main.bounds)
self.remoteRenderer.videoContentMode = .scaleAspectFill
self.localRenderer = RTCMTLVideoView(frame: UIScreen.main.bounds)
self.localRenderer.videoContentMode = .scaleAspectFill
self.localRenderer.transform = CGAffineTransformMakeScale(-1.0, 1.0) //local video should be displayed as "mirrored"
self.ringingPlayer = try! AVAudioPlayer(contentsOf:Bundle.main.url(forResource:"ringing", withExtension:"wav", subdirectory:"CallSounds")!)
self.busyPlayer = try! AVAudioPlayer(contentsOf:Bundle.main.url(forResource:"busy", withExtension:"wav", subdirectory:"CallSounds")!)
self.errorPlayer = try! AVAudioPlayer(contentsOf:Bundle.main.url(forResource:"error", withExtension:"wav", subdirectory:"CallSounds")!)
}
func maybeStartRenderer() {
if MLCallType(rawValue:call.callType) == .video && MLCallState(rawValue:call.state) == .connected {
DDLogInfo("Starting local and remote video renderers...")
call.obj.startCaptureLocalVideo(withRenderer: self.localRenderer, andCameraPosition:cameraPosition)
call.obj.renderRemoteVideo(withRenderer: self.remoteRenderer)
}
}
func handleStateChange(_ state:MLCallState, _ audioState:MLAudioState) {
switch state {
case .unknown:
DDLogDebug("state: unknown")
ringingPlayer.stop()
busyPlayer.stop()
errorPlayer.play()
case .discovering:
DDLogDebug("state: discovering")
ringingPlayer.stop()
busyPlayer.stop()
errorPlayer.stop()
case .ringing:
DDLogDebug("state: ringing")
busyPlayer.stop()
errorPlayer.stop()
ringingPlayer.play()
case .connecting:
DDLogDebug("state: connecting")
ringingPlayer.stop()
busyPlayer.stop()
errorPlayer.stop()
case .reconnecting:
DDLogDebug("state: reconnecting")
ringingPlayer.stop()
busyPlayer.stop()
errorPlayer.stop()
case .connected:
DDLogDebug("state: connected")
maybeStartRenderer()
case .finished:
DDLogDebug("state: finished: \(String(describing:call.finishReason as NSNumber))")
//check audio state before trying to play anything (if we are still in state .call,
//callkit will deactivate this audio session shortly, stopping our players)
if audioState == .normal {
switch MLCallFinishReason(rawValue:call.finishReason) {
case .unknown:
DDLogDebug("state: finished: unknown")
ringingPlayer.stop()
busyPlayer.stop()
errorPlayer.stop()
case .connectivityError:
DDLogDebug("state: finished: connectivityError")
ringingPlayer.stop()
busyPlayer.stop()
errorPlayer.play()
case .securityError:
DDLogDebug("state: finished: securityError")
ringingPlayer.stop()
busyPlayer.stop()
errorPlayer.play()
case .unanswered:
DDLogDebug("state: finished: unanswered")
ringingPlayer.stop()
errorPlayer.stop()
busyPlayer.play()
case .retracted:
DDLogDebug("state: finished: retracted")
ringingPlayer.stop()
errorPlayer.stop()
busyPlayer.play()
case .rejected:
DDLogDebug("state: finished: rejected")
ringingPlayer.stop()
errorPlayer.stop()
busyPlayer.play()
case .declined:
DDLogDebug("state: finished: declined")
ringingPlayer.stop()
errorPlayer.stop()
busyPlayer.play()
case .error:
DDLogDebug("state: finished: error")
ringingPlayer.stop()
busyPlayer.stop()
errorPlayer.play()
// case .normal:
// case .answeredElsewhere:
default:
DDLogDebug("state: finished: default")
ringingPlayer.stop()
busyPlayer.stop()
errorPlayer.stop()
}
}
default:
DDLogDebug("state: default")
}
}
var body: some View {
ZStack {
Color.background
.ignoresSafeArea()
if MLCallType(rawValue:call.callType) == .video && MLCallState(rawValue:call.state) == .connected {
VideoView(renderer:self.remoteRenderer)
ZStack {
VideoView(renderer:self.localRenderer)
//this will sometimes only honor the width and ignore the height
.frame(width: UIScreen.main.bounds.size.width/5.0, height: UIScreen.main.bounds.size.height/5.0)
if controlsVisible {
Button(action: {
if cameraPosition == .front {
cameraPosition = .back
} else {
cameraPosition = .front
}
call.obj.stopCaptureLocalVideo()
maybeStartRenderer()
}, label: {
Image(systemName: "arrow.triangle.2.circlepath.camera.fill")
.resizable()
.frame(width: 32.0, height: 32.0)
.foregroundColor(.primary)
})
}
}
.position(localRendererLocation)
.gesture(DragGesture().onChanged { value in
self.localRendererLocation = value.location
})
.onTapGesture(count: 2) {
if sendingVideo {
call.obj.hideVideo()
} else {
call.obj.showVideo()
}
sendingVideo = !sendingVideo
}
}
if MLCallType(rawValue:call.callType) == .audio ||
(MLCallType(rawValue:call.callType) == .video && (MLCallState(rawValue:call.state) != .connected || controlsVisible)) {
VStack {
Group {
Spacer().frame(height: 24)
HStack(alignment: .top) {
Spacer().frame(width:20)
VStack {
Spacer().frame(height: 8)
switch MLCallDirection(rawValue:call.direction) {
case .incoming:
Image(systemName: "phone.arrow.down.left")
.resizable()
.frame(width: 20.0, height: 20.0)
.foregroundColor(.primary)
case .outgoing:
Image(systemName: "phone.arrow.up.right")
.resizable()
.frame(width: 20.0, height: 20.0)
.foregroundColor(.primary)
default: //should never be reached
Text("")
}
}
VStack {
Spacer().frame(height: 8)
Button(action: {
//show dialog explaining different encryption states
self.showSecurityHelpAlert = MLCallEncryptionState(rawValue:call.encryptionState)
}, label: {
switch MLCallEncryptionState(rawValue:call.encryptionState) {
case .unknown:
Text("")
case .clear:
Spacer().frame(width: 10)
Image(systemName: "xmark.shield.fill")
.resizable()
.frame(width: 20.0, height: 20.0)
.foregroundColor(.red)
case .toFU:
Spacer().frame(width: 10)
Image(systemName: "checkmark.shield.fill")
.resizable()
.frame(width: 20.0, height: 20.0)
.foregroundColor(.yellow)
case .trusted:
Spacer().frame(width: 10)
Image(systemName: "checkmark.shield.fill")
.resizable()
.frame(width: 20.0, height: 20.0)
.foregroundColor(.green)
default: //should never be reached
Text("")
}
})
}
Spacer()
Text(contact.contactDisplayName as String)
.font(.largeTitle)
.foregroundColor(.primary)
Spacer()
VStack {
Spacer().frame(height: 8)
Button(action: {
if let activeChats = self.appDelegate.obj.activeChats {
//make sure we don't animate anything
activeChats.dismissCompleteViewChain(withAnimation: false) {
activeChats.presentChat(with:self.contact.obj)
}
} else {
//self.delegate.dismissWithoutAnimation()
unreachable("active chats should always be accessible from AVCallUI!")
}
}, label: {
Image(systemName: "text.bubble")
.resizable()
.frame(width: 28.0, height: 28.0)
.foregroundColor(.primary)
})
}
Spacer().frame(width:20)
}
Spacer().frame(height: 16)
//this is needed because ObservableKVOWrapper somehow extracts an NSNumber? from it's wrapped object
//which results in a runtime error when trying to cast NSNumber? to MLCallState
switch MLCallState(rawValue:call.state) {
case .discovering:
Text("Discovering devices...")
.bold()
.foregroundColor(.primary)
case .ringing:
Text("Ringing...")
.bold()
.foregroundColor(.primary)
case .connecting:
Text("Connecting...")
.bold()
.foregroundColor(.primary)
case .reconnecting:
Text("Reconnecting...")
.bold()
.foregroundColor(.primary)
case .connected:
Text("Connected: \(formatter.string(from: TimeInterval(call.durationTime as UInt))!)")
.bold()
.foregroundColor(.primary)
case .finished:
switch MLCallFinishReason(rawValue:call.finishReason) {
case .unknown:
Text("Call ended for an unknown reason")
.bold()
.foregroundColor(.primary)
case .normal:
if call.wasConnectedOnce {
Text("Call ended, duration: \(formatter.string(from: TimeInterval(call.durationTime as UInt))!)")
.bold()
.foregroundColor(.primary)
} else {
Text("Call ended")
.bold()
.foregroundColor(.primary)
}
case .connectivityError:
if call.wasConnectedOnce {
Text("Call ended: connection failed\nDuration: \(formatter.string(from: TimeInterval(call.durationTime as UInt))!)")
.bold()
.foregroundColor(.primary)
} else {
Text("Call ended: connection failed")
.bold()
.foregroundColor(.primary)
}
case .securityError:
Text("Call ended: couldn't establish encryption")
.bold()
.foregroundColor(.primary)
case .unanswered:
Text("Call was not answered")
.bold()
.foregroundColor(.primary)
case .answeredElsewhere:
Text("Call ended: answered with other device")
.bold()
.foregroundColor(.primary)
case .retracted:
//this will only be displayed for timer-induced retractions,
//reflect that in our text instead of using some generic "hung up"
//Text("Call ended: hung up")
Text("Call ended: remote busy")
.bold()
.foregroundColor(.primary)
case .rejected:
Text("Call ended: remote busy")
.bold()
.foregroundColor(.primary)
case .declined:
Text("Call ended: declined")
.bold()
.foregroundColor(.primary)
case .error:
Text("Call ended: application error")
.bold()
.foregroundColor(.primary)
default: //should never be reached
Text("")
}
default: //should never be reached
Text("")
}
Spacer().frame(height: 48)
if MLCallType(rawValue:call.callType) == .audio || MLCallState(rawValue:call.state) != .connected {
Image(uiImage: contact.avatar)
.resizable()
.frame(minWidth: 100, idealWidth: 150, maxWidth: 200, minHeight: 100, idealHeight: 150, maxHeight: 200, alignment: .center)
.scaledToFit()
.shadow(radius: 7)
}
Spacer()
}
if MLCallState(rawValue:call.state) == .finished {
HStack() {
Spacer()
Button(action: {
self.delegate.dismissWithoutAnimation()
if let activeChats = self.appDelegate.obj.activeChats {
activeChats.call(contact.obj, with:MLCallType(rawValue:call.callType)!)
}
}) {
Image(systemName: "arrow.clockwise.circle.fill")
.resizable()
.frame(width: 64.0, height: 64.0)
.symbolRenderingMode(.palette)
.foregroundStyle(.white, .green)
.shadow(radius: 7)
}
.buttonStyle(BorderlessButtonStyle())
Spacer().frame(width: 64)
Button(action: {
delegate.dismissWithoutAnimation()
}) {
Image(systemName: "x.circle.fill")
.resizable()
.frame(width: 64.0, height: 64.0)
.symbolRenderingMode(.palette)
.foregroundStyle(.white, .red)
.shadow(radius: 7)
}
.buttonStyle(BorderlessButtonStyle())
Spacer()
}
} else {
HStack() {
Spacer()
if MLCallState(rawValue:call.state) == .connected || MLCallState(rawValue:call.state) == .reconnecting {
Button(action: {
call.muted = !call.muted
}) {
Image(systemName: "mic.slash.circle.fill")
.resizable()
.frame(width: 64.0, height: 64.0)
.symbolRenderingMode(.palette)
.foregroundStyle(call.muted ? .black : .white, call.muted ? .white : .black)
.shadow(radius: 7)
}
.buttonStyle(BorderlessButtonStyle())
Spacer().frame(width: 32)
}
Button(action: {
call.obj.end()
self.delegate.dismissWithoutAnimation()
}) {
Image(systemName: "phone.down.circle.fill")
.resizable()
.frame(width: 64.0, height: 64.0)
.symbolRenderingMode(.palette)
.foregroundStyle(.white, .red)
.shadow(radius: 7)
}
.buttonStyle(BorderlessButtonStyle())
if MLCallState(rawValue:call.state) == .connected || MLCallState(rawValue:call.state) == .reconnecting {
Spacer().frame(width: 32)
Button(action: {
call.speaker = !call.speaker
}) {
Image(systemName: "speaker.wave.2.circle.fill")
.resizable()
.frame(width: 64.0, height: 64.0)
.symbolRenderingMode(.palette)
.foregroundStyle(call.speaker ? .black : .white, call.speaker ? .white : .black)
.shadow(radius: 7)
}
.buttonStyle(BorderlessButtonStyle())
}
Spacer()
}
}
Spacer().frame(height: 32)
}
}
}
.onTapGesture(count: 1) {
controlsVisible = !controlsVisible
}
.alert(isPresented: $showMicAlert) {
Alert(
title: Text("Missing permission"),
message: Text("You need to grant microphone access in iOS Settings-> Privacy-> Microphone, if you want that others can hear you."),
dismissButton: .default(Text("OK"))
)
}
.richAlert(isPresented:$showSecurityHelpAlert, title:Text("Call security help")) {
VStack(alignment: .leading) {
HStack {
Image(systemName: "xmark.shield.fill")
.resizable()
.frame(width: 20.0, height: 20.0)
.foregroundColor(.red)
Spacer().frame(width: 10)
Text("Red x-mark shield:")
}.font(Font.body.weight(showSecurityHelpAlert == .clear ? .heavy : .medium))
Text("This means your call is encrypted, but the remote party could not be verified using OMEMO encryption.\nYour or the callee's XMPP server could possibly Man-In-The-Middle you.")
Spacer().frame(height: 20)
HStack {
Image(systemName: "checkmark.shield.fill")
.resizable()
.frame(width: 20.0, height: 20.0)
.foregroundColor(.yellow)
Spacer().frame(width: 10)
Text("Yellow checkmark shield:")
}.font(Font.body.weight(showSecurityHelpAlert == .toFU ? .heavy : .medium))
Text("This means your call is encrypted and the remote party was verified using OMEMO encryption.\nBut since you did not manually verify the callee's OMEMO fingerprints, your or the callee's XMPP server could possibly have inserted their own OMEMO keys to Man-In-The-Middle you.")
Spacer().frame(height: 20)
HStack {
Image(systemName: "checkmark.shield.fill")
.resizable()
.frame(width: 20.0, height: 20.0)
.foregroundColor(.green)
Spacer().frame(width: 10)
Text("Green checkmark shield:")
}.font(Font.body.weight(showSecurityHelpAlert == .trusted ? .heavy : .medium))
Text("This means your call is encrypted and the remote party was verified using OMEMO encryption.\nYou manually verified the used OMEMO keys and no Man-In-The-Middle can take place.")
Spacer().frame(height: 20)
}
}
.onAppear {
//force portrait mode and lock ui there
UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
self.appDelegate.obj.orientationLock = .portrait
UIApplication.shared.isIdleTimerDisabled = true
self.ringingPlayer.numberOfLoops = -1
self.busyPlayer.numberOfLoops = -1
self.errorPlayer.numberOfLoops = -1
//ask for mic permissions
AVAudioSession.sharedInstance().requestRecordPermission { granted in
if !granted {
showMicAlert = true
}
}
maybeStartRenderer()
}
.onDisappear {
//allow all orientations again
self.appDelegate.obj.orientationLock = .all
UIApplication.shared.isIdleTimerDisabled = false
ringingPlayer.stop()
busyPlayer.stop()
errorPlayer.stop()
if MLCallType(rawValue:call.callType) == .video {
call.obj.stopCaptureLocalVideo()
}
}
.onChange(of: MLCallState(rawValue:call.state)) { state in
DDLogVerbose("call state changed: \(String(describing:call.state as NSNumber))")
handleStateChange(call.obj.state, appDelegate.obj.audioState)
}
.onChange(of: MLAudioState(rawValue:appDelegate.audioState)) { audioState in
DDLogVerbose("audioState changed: \(String(describing:appDelegate.audioState as NSNumber))")
handleStateChange(call.obj.state, appDelegate.obj.audioState)
}
}
}
struct AVCallUI_Previews: PreviewProvider {
static var delegate = SheetDismisserProtocol()
static var previews: some View {
AVCallUI(delegate:delegate, call:MLCall.makeDummyCall(0))
}
}

View file

@ -0,0 +1,20 @@
//
// AccountListController.h
// Monal
//
// Created by Anurodh Pokharel on 6/14/13.
//
//
#import <UIKit/UIKit.h>
#import <Monal-Swift.h>
#import "MLSwitchCell.h"
@interface AccountListController : UITableViewController
-(NSUInteger) getAccountNum;
-(NSNumber*) getAccountIDByIndex:(NSUInteger) index;
-(void) setupAccountsView;
-(void) refreshAccountList;
-(void) initContactCell:(MLSwitchCell*) cell forAccNo:(NSUInteger) accNo;
@end

View file

@ -0,0 +1,114 @@
//
// AccountListController.m
// Monal
//
// Created by Anurodh Pokharel on 6/14/13.
//
//
#import "AccountListController.h"
#import "DataLayer.h"
#import "MLXMPPManager.h"
#import "HelperTools.h"
@interface AccountListController ()
@property (nonatomic, strong) NSDateFormatter* uptimeFormatter;
@property (nonatomic, strong) NSIndexPath* selected; // User-selected account - needed for segue
@property (nonatomic, strong) UITableView* accountsTable;
@property (nonatomic, strong) NSArray<NSDictionary*>* accountList;
@end
@implementation AccountListController
#pragma mark View life cycle
- (void) setupAccountsView
{
// Do any additional setup after loading the view.
self.accountsTable.backgroundView = nil;
self.accountsTable = self.tableView;
self.accountsTable.delegate = self;
self.accountsTable.dataSource = self;
dispatch_async(dispatch_get_main_queue(), ^{
[self.accountsTable reloadData];
});
self.uptimeFormatter = [NSDateFormatter new];
self.uptimeFormatter.dateStyle = NSDateFormatterShortStyle;
self.uptimeFormatter.timeStyle = NSDateFormatterShortStyle;
self.uptimeFormatter.doesRelativeDateFormatting = YES;
self.uptimeFormatter.locale = [NSLocale currentLocale];
self.uptimeFormatter.timeZone = [NSTimeZone systemTimeZone];
NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self selector:@selector(refreshAccountList) name:kMonalAccountStatusChanged object:nil];
}
-(void) dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
-(NSUInteger) getAccountNum
{
return self.accountList.count;
}
-(NSNumber*) getAccountIDByIndex:(NSUInteger) index
{
NSNumber* result = [[self.accountList objectAtIndex: index] objectForKey:@"account_id"];
MLAssert(result != nil, @"getAccountIDByIndex, result should not be nil");
return result;
}
-(void) refreshAccountList
{
NSArray* accountList = [[DataLayer sharedInstance] accountList];
dispatch_async(dispatch_get_main_queue(), ^{
self.accountList = accountList;
[self.accountsTable reloadData];
});
}
-(void) initContactCell:(MLSwitchCell*) cell forAccNo:(NSUInteger) accNo
{
[cell initTapCell:@"\n\n"];
NSDictionary* account = [self.accountList objectAtIndex:accNo];
MLAssert(account != nil, ([NSString stringWithFormat:@"Expected non nil account in row %lu", (unsigned long)accNo]));
if([(NSString*)[account objectForKey:@"domain"] length] > 0) {
cell.textLabel.text = [NSString stringWithFormat:@"%@@%@", [[self.accountList objectAtIndex:accNo] objectForKey:@"username"],
[[self.accountList objectAtIndex:accNo] objectForKey:@"domain"]];
}
else
{
cell.textLabel.text = [[self.accountList objectAtIndex:accNo] objectForKey:@"username"];
}
UIImageView* accessory = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 30, 30)];
if([[account objectForKey:@"enabled"] boolValue] == YES)
{
cell.imageView.image = [UIImage systemImageNamed:@"checkmark.circle"];
if([[MLXMPPManager sharedInstance] isAccountForIdConnected:[[self.accountList objectAtIndex:accNo] objectForKey:@"account_id"]])
{
accessory.image = [UIImage imageNamed:@"Connected"];
cell.accessoryView = accessory;
}
else
{
accessory.image = [UIImage imageNamed:@"Disconnected"];
cell.accessoryView = accessory;
}
}
else
{
cell.imageView.image = [UIImage systemImageNamed:@"circle"];
accessory.image = nil;
cell.accessoryView = accessory;
}
}
@end

View file

@ -0,0 +1,66 @@
//
// AccountPicker.swift
// Monal
//
// Created by Thilo Molitor on 20.01.23.
// Copyright © 2023 monal-im.org. All rights reserved.
//
struct AccountPicker: View {
let contacts: [MLContact]
let callType: MLCallType
#if IS_ALPHA
let appLogoId = "AlphaAppLogo"
#elseif IS_QUICKSY
let appLogoId = "QuicksyAppLogo"
#else
let appLogoId = "AppLogo"
#endif
init(contacts:[MLContact], callType: MLCallType) {
self.contacts = contacts
self.callType = callType
}
var body: some View {
//ScrollView {
VStack {
HStack () {
Image(decorative: appLogoId)
.resizable()
.frame(width: CGFloat(120), height: CGFloat(120), alignment: .center)
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
.padding()
Text("You are trying to call '\(contacts.first!.contactDisplayName)' (\(contacts.first!.contactJid)), but this contact can be reached using different accounts. Please select the account you want to place the outgoing call with.")
.padding()
.padding(.leading, -16.0)
}
.frame(maxWidth: .infinity)
.background(Color(UIColor.systemBackground))
List {
ForEach(contacts) { contact in
if let accountEntry = DataLayer.sharedInstance().details(forAccount:contact.accountID) {
let accountJid = "\(accountEntry["username"] ?? "<unknown>" as NSString)@\(accountEntry["domain"] ?? "<unknown>" as NSString)"
let accountContact = MLContact.createContact(fromJid:accountJid, andAccountID:accountEntry["account_id"] as! NSNumber)
Button {
(UIApplication.shared.delegate as! MonalAppDelegate).activeChats!.call(contact, with:callType)
} label: {
ContactEntry(contact:ObservableKVOWrapper(accountContact), selfnotesPrefix:false)
}
}
}
}
.listStyle(.insetGrouped)
}
//}
.textFieldStyle(.roundedBorder)
.navigationBarTitle(Text("Account Picker"))
}
}
struct AccountPicker_Previews: PreviewProvider {
static var previews: some View {
AccountPicker(contacts:[MLContact.makeDummyContact(0)], callType:.audio)
}
}

View file

@ -0,0 +1,64 @@
//
// ActiveChatsViewController.h
// Monal
//
// Created by Anurodh Pokharel on 6/14/13.
//
//
#import <UIKit/UIKit.h>
#import "MLConstants.h"
#import "MLContact.h"
#import "MLCall.h"
#import <DZNEmptyDataSet/UIScrollView+EmptyDataSet.h>
NS_ASSUME_NONNULL_BEGIN
@class UIHostingControllerWorkaround;
@class chatViewController;
@class MLCall;
@interface SizeClassWrapper: NSObject
@property (atomic) UIUserInterfaceSizeClass horizontal;
@end
@interface ActiveChatsViewController : UITableViewController <DZNEmptyDataSetSource, DZNEmptyDataSetDelegate>
@property (nonatomic, strong) UITableView* chatListTable;
@property (nonatomic, weak) IBOutlet UIBarButtonItem* settingsButton;
@property (weak, nonatomic) IBOutlet UIBarButtonItem* spinnerButton;
@property (nonatomic, weak) IBOutlet UIBarButtonItem* composeButton;
@property (nonatomic, strong) UIActivityIndicatorView* spinner;
@property (atomic, strong) SizeClassWrapper* sizeClass;
@property (atomic, readonly) chatViewController* _Nullable currentChatView;
-(void) showCallContactNotFoundAlert:(NSString*) jid;
-(void) callContact:(MLContact*) contact withUIKitSender:(_Nullable id) sender;
-(void) callContact:(MLContact*) contact withCallType:(MLCallType) callType;
-(void) presentAccountPickerForContacts:(NSArray<MLContact*>*) contacts andCallType:(MLCallType) callType;
-(void) presentCall:(MLCall*) call;
-(void) presentChatWithContact:(MLContact* _Nullable) contact;
-(void) presentChatWithContact:(MLContact* _Nullable) contact andCompletion:(monal_id_block_t _Nullable) completion;
-(void) presentSplitPlaceholder;
-(void) refreshDisplay;
-(void) showContacts;
-(void) deleteConversation;
-(void) showSettings;
-(void) showGeneralSettings;
-(void) prependGeneralSettings;
-(void) showNotificationSettings;
-(void) showDetails;
-(void) showRegisterWithUsername:(NSString*) username onHost:(NSString*) host withToken:(NSString* _Nullable) token usingCompletion:(monal_id_block_t _Nullable) callback;
-(void) showAddContactWithJid:(NSString*) jid preauthToken:(NSString* _Nullable) preauthToken prefillAccount:(xmpp* _Nullable) account andOmemoFingerprints:(NSDictionary* _Nullable) fingerprints;
-(void) showAddContact;
-(void) sheetDismissed;
-(void) segueToIntroScreensIfNeeded;
-(void) resetViewQueue;
-(void) dismissCompleteViewChainWithAnimation:(BOOL) animation andCompletion:(monal_void_block_t _Nullable) completion;
-(void) updateSizeClass;
@end
NS_ASSUME_NONNULL_END

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,347 @@
//
// AddContactMenu.swift
// Monal
//
// Created by Jan on 27.10.22.
// Copyright © 2022 monal-im.org. All rights reserved.
//
import MobileCoreServices
import UniformTypeIdentifiers
struct AddContactMenu: View {
var delegate: SheetDismisserProtocol
static private let jidFaultyPattern = "^([^@]+@)?.+(\\..{2,})?$"
@State private var enabledAccounts: [xmpp]
@State private var selectedAccount: Int
@State private var scannedFingerprints: [NSNumber:Data]? = nil
@State private var importScannedFingerprints: Bool = false
@State private var toAdd: String = ""
@State private var showInvitationError = false
@State private var showAlert = false
// note: dismissLabel is not accessed but defined at the .alert() section
@State private var alertPrompt = AlertPrompt(dismissLabel: Text("Close"))
@State private var invitationResult: [String:AnyObject]? = nil
@StateObject private var overlay = LoadingOverlayState()
@State private var showQRCodeScanner = false
@State private var success = false
@State private var newContact : MLContact?
@State private var isEditingJid = false
private let dismissWithNewContact: (MLContact) -> ()
private let preauthToken: String?
init(delegate: SheetDismisserProtocol, dismissWithNewContact: @escaping (MLContact) -> (), prefillJid: String = "", preauthToken:String? = nil, prefillAccount:xmpp? = nil, omemoFingerprints: [NSNumber:Data]? = nil) {
self.delegate = delegate
self.dismissWithNewContact = dismissWithNewContact
//self.toAdd = State(wrappedValue: prefillJid)
self.toAdd = prefillJid
self.preauthToken = preauthToken
//only display omemo ui part if there are any fingerprints (the checks below test for nil, not for 0)
if omemoFingerprints?.count ?? 0 > 0 {
self.scannedFingerprints = omemoFingerprints
}
let enabledAccounts = MLXMPPManager.sharedInstance().connectedXMPP as! [xmpp]
self.enabledAccounts = enabledAccounts
self.selectedAccount = enabledAccounts.first != nil ? 0 : -1;
if let prefillAccount = prefillAccount {
for index in enabledAccounts.indices {
if enabledAccounts[index].accountID.isEqual(to:prefillAccount.accountID) {
self.selectedAccount = index
}
}
}
}
// FIXME duplicate code from WelcomeLogIn.swift, maybe move to SwiftuiHelpers
private var toAddEmptyAlert: Bool {
alertPrompt.title = Text("No Empty Values!")
alertPrompt.message = Text("Please make sure you have entered a valid jid.")
return toAddEmpty
}
private var toAddInvalidAlert: Bool {
alertPrompt.title = Text("Invalid Credentials!")
alertPrompt.message = Text("The jid you want to add should be in in the format user@domain.tld.")
return toAddInvalid
}
private func errorAlert(title: Text, message: Text = Text("")) {
alertPrompt.title = title
alertPrompt.message = message
showAlert = true
}
private func successAlert(title: Text, message: Text) {
alertPrompt.title = title
alertPrompt.message = message
self.success = true // < dismiss entire view on close
showAlert = true
}
private var toAddEmpty: Bool {
return toAdd.isEmpty
}
private var toAddInvalid: Bool {
return toAdd.range(of: AddContactMenu.jidFaultyPattern, options:.regularExpression) == nil
}
func trustFingerprints(_ fingerprints:[NSNumber:Data]?, for jid:String, on account:xmpp) {
//we don't untrust other devices not included in here, because conversations only exports its own fingerprint
if let fingerprints = fingerprints {
for (deviceId, fingerprint) in fingerprints {
let address = SignalAddress.init(name:jid, deviceId:deviceId.int32Value)
let knownDevices = Array(account.omemo.knownDevices(forAddressName:jid))
if !knownDevices.contains(deviceId) {
account.omemo.addIdentityManually(address, identityKey:fingerprint)
assert(account.omemo.getIdentityFor(address) == fingerprint, "The stored and created fingerprint should match")
}
//trust device/fingerprint if fingerprints match
let knownFingerprintHex = HelperTools.signalHexKey(with:account.omemo.getIdentityFor(address))
let addedFingerprintHex = HelperTools.signalHexKey(with:fingerprint)
if knownFingerprintHex.uppercased() == addedFingerprintHex.uppercased() {
account.omemo.updateTrust(true, for:address)
}
}
}
}
func addJid(jid: String) {
let account = self.enabledAccounts[selectedAccount]
let contact = MLContact.createContact(fromJid: jid, andAccountID: account.accountID)
if contact.isInRoster {
self.newContact = contact
//import omemo fingerprints as manually trusted, if requested
trustFingerprints(self.importScannedFingerprints ? self.scannedFingerprints : [:], for:jid, on:account)
//only alert of already known contact if we did not import the omemo fingerprints
if !self.importScannedFingerprints || self.scannedFingerprints?.count ?? 0 == 0 {
if self.enabledAccounts.count > 1 {
self.success = true
successAlert(title: Text("Already present"), message: Text("This contact is already in the contact list of the selected account"))
} else {
self.success = true
successAlert(title: Text("Already present"), message: Text("This contact is already in your contact list"))
}
}
return
}
showPromisingLoadingOverlay(overlay, headline:NSLocalizedString("Adding...", comment: ""), description:"") {
account.checkJidType(jid)
}.done { type in
let type = type as! String
if type == "account" {
let contact = MLContact.createContact(fromJid: jid, andAccountID: account.accountID)
self.newContact = contact
MLXMPPManager.sharedInstance().add(contact, withPreauthToken:preauthToken)
//import omemo fingerprints as manually trusted, if requested
trustFingerprints(self.importScannedFingerprints ? self.scannedFingerprints : [:], for:jid, on:account)
successAlert(title: Text("Permission Requested"), message: Text("The new contact will be added to your contacts list when the person you've added has approved your request."))
} else if type == "muc" {
showPromisingLoadingOverlay(overlay, headlineView:Text("Adding Group/Channel..."), descriptionView:Text("")) {
promisifyMucAction(account:account, mucJid:jid) {
account.joinMuc(jid)
}
}.done { _ in
self.newContact = MLContact.createContact(fromJid: jid, andAccountID: account.accountID)
successAlert(title: Text("Success!"), message: Text("Successfully joined group/channel \(jid)!"))
}.catch { error in
errorAlert(title: Text("Error entering group/channel!"), message: Text("\(String(describing:error))"))
}
}
}.catch { error in
errorAlert(title: Text("Error"), message: Text(error.localizedDescription))
}
}
var body: some View {
let account = self.enabledAccounts[selectedAccount]
let splitJid = HelperTools.splitJid(account.connectionProperties.identity.jid)
Form {
if enabledAccounts.isEmpty {
Text("Please make sure at least one account has connected before trying to add a contact or channel.")
.foregroundColor(.secondary)
}
else
{
if DataLayer.sharedInstance().allContactRequests().count > 0 {
ContactRequestsMenu()
}
Section(header:Text("Contact and Group/Channel Jids are usually in the format: name@domain.tld")) {
if enabledAccounts.count > 1 {
Picker("Use account", selection: $selectedAccount) {
ForEach(Array(self.enabledAccounts.enumerated()), id: \.element) { idx, account in
Text(account.connectionProperties.identity.jid).tag(idx)
}
}
.pickerStyle(.menu)
}
TextField(NSLocalizedString("Contact-, Group- or Channel-Jid", comment: "placeholder when adding jid"), text: $toAdd, onEditingChanged: { isEditingJid = $0 })
.textInputAutocapitalization(.never)
.autocapitalization(.none)
.autocorrectionDisabled()
.keyboardType(.emailAddress)
.addClearButton(isEditing: isEditingJid, text:$toAdd)
.disabled(scannedFingerprints != nil)
.foregroundColor(scannedFingerprints != nil ? .secondary : .primary)
.onChange(of: toAdd) { _ in toAdd = toAdd.replacingOccurrences(of: " ", with: "") }
if scannedFingerprints != nil && scannedFingerprints!.count > 0 {
Section(header: Text("A contact was scanned through the QR code scanner")) {
Toggle(isOn: $importScannedFingerprints) {
Text("Import and trust OMEMO fingerprints from QR code")
}
}
}
if scannedFingerprints != nil {
Button(action: {
toAdd = ""
importScannedFingerprints = true
scannedFingerprints = nil
}, label: {
Text("Clear scanned contact")
.foregroundColor(.red)
})
}
HStack {
Spacer()
Button(action: {
showAlert = toAddEmptyAlert || toAddInvalidAlert
if !showAlert {
let jidComponents = HelperTools.splitJid(toAdd)
if jidComponents["host"] == nil || jidComponents["host"]!.isEmpty {
errorAlert(title: Text("Error"), message: Text("Something went wrong while parsing your input..."))
showAlert = true
return
}
// use the canonized jid from now on (lowercased, resource removed etc.)
addJid(jid: jidComponents["user"]!)
}
}) {
scannedFingerprints == nil ? Text("Add") : Text("Add scanned contact")
}
.disabled(toAddEmpty || toAddInvalid)
.buttonStyle(MonalProminentButtonStyle())
}
}
if DataLayer.sharedInstance().allContactRequests().count == 0 {
Section {
ContactRequestsMenu()
}
}
}
}
.padding()
.alert(isPresented: $showAlert) {
Alert(title: alertPrompt.title, message: alertPrompt.message, dismissButton:.default(Text("Close"), action: {
showAlert = false
if self.success == true {
if self.newContact != nil {
self.dismissWithNewContact(newContact!)
} else {
self.delegate.dismiss()
}
}
}))
}
.richAlert(isPresented: $invitationResult, title:Text("Invitation for \(splitJid["host"]!) created")) { data in
VStack {
Image(uiImage: createQrCode(value: data["landing"] as! String))
.interpolation(.none)
.resizable()
.scaledToFit()
.aspectRatio(1, contentMode: .fit)
if let expires = data["expires"] as? Date {
Text("This invitation will expire on \(expires.formatted(date:.numeric, time:.shortened))")
.font(.footnote)
.multilineTextAlignment(.leading)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
} buttons: { data in
Button(action: {
UIPasteboard.general.setValue(data["landing"] as! String, forPasteboardType:UTType.utf8PlainText.identifier as String)
invitationResult = nil
}) {
ShareLink("Share invitation link", item: URL(string: data["landing"] as! String)!)
}
Button(action: {
invitationResult = nil
}) {
Text("Close")
.frame(maxWidth: .infinity)
}
}
.sheet(isPresented: $showQRCodeScanner) {
NavigationStack {
MLQRCodeScanner(handleClose: {
self.showQRCodeScanner = false
})
.navigationTitle("QR-Code Scanner")
.navigationBarTitleDisplayMode(.inline)
.toolbar(content: {
ToolbarItem(placement: .navigationBarLeading, content: {
Button(action: {
self.showQRCodeScanner = false
}, label: {
Text("Close")
})
})
})
}
}
.navigationBarTitle(Text("Add Contact or Channel"), displayMode: .inline)
.toolbar(content: {
ToolbarItemGroup(placement: .navigationBarTrailing) {
if account.connectionProperties.discoveredAdhocCommands["urn:xmpp:invite#invite"] != nil {
Button(action: {
DDLogVerbose("Trying to create invitation for: \(String(describing:splitJid["host"]!))")
showLoadingOverlay(overlay, headline: NSLocalizedString("Creating invitation...", comment: ""))
account.createInvitation(completion: {
let result = $0 as! [String:AnyObject]
DispatchQueue.main.async {
hideLoadingOverlay(overlay)
DDLogVerbose("Got invitation result: \(String(describing:result))")
if result["success"] as! Bool == true {
invitationResult = result
} else {
errorAlert(title:Text("Failed to create invitation for \(splitJid["host"]!)"), message:Text(result["error"] as! String))
}
}
})
}, label: {
Image(systemName: "square.and.arrow.up")
})
}
Button(action: {
self.showQRCodeScanner = true
}, label: {
Image(systemName: "camera.fill")
})
}
})
.addLoadingOverlay(overlay)
}
}
struct AddContactMenu_Previews: PreviewProvider {
static var delegate = SheetDismisserProtocol()
static var previews: some View {
AddContactMenu(delegate: delegate, dismissWithNewContact: { c in
})
}
}

View file

@ -0,0 +1,100 @@
//
// BackgroundSettings.swift
// Monal
//
// Created by Thilo Molitor on 14.11.22.
// Copyright © 2022 monal-im.org. All rights reserved.
//
//swiftui is somehow needed to let the PhotosUI import succeed, even if it's already imported by SwiftuiHelpers.swift using @_exported
import SwiftUI
import PhotosUI
@ViewBuilder
func title(contact: ObservableKVOWrapper<MLContact>?) -> some View {
if let contact = contact {
Text("Select a background to display behind conversations with \(contact.contactDisplayName as String)")
} else {
Text("Select a default background to display behind conversations.")
}
}
struct BackgroundSettings: View {
@State private var selectedItem: PhotosPickerItem? = nil
@State private var showingImagePicker = false
@State private var inputImage: UIImage?
let contact: ObservableKVOWrapper<MLContact>?
init(contact: ObservableKVOWrapper<MLContact>?) {
self.contact = contact
_inputImage = State(initialValue:MLImageManager.sharedInstance().getBackgroundFor(self.contact?.obj))
}
var body: some View {
VStack {
Form {
Section(header:title(contact:contact)) {
VStack(spacing: 20) {
Spacer().frame(height: 0)
PhotosPicker(selection:$selectedItem, matching:.images, photoLibrary:.shared()) {
if let inputImage = inputImage {
HStack(alignment: .center) {
Image(uiImage:inputImage)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: .infinity, alignment: .center)
}
.addTopRight {
Button(action: {
self.inputImage = nil
}, label: {
Image(systemName: "xmark.circle.fill")
.resizable()
.frame(width: 32.0, height: 32.0)
.accessibilityLabel(Text("Remove Background Image"))
.symbolRenderingMode(.palette)
.foregroundStyle(.white, .red)
})
.buttonStyle(.borderless)
.offset(x: 12, y: -12)
}
} else {
Text("Select background image")
.frame(maxWidth: .infinity, alignment: .center)
}
}
.accessibilityLabel(Text("Change Background Image"))
.onChange(of:selectedItem) { newItem in
// Retrive selected asset in the form of Data
newItem?.loadTransferable(type:Data.self) { result in
guard let data = try? result.get() else {
self.inputImage = nil
return
}
guard let loadedImage = UIImage(data: data) else {
self.inputImage = nil
return
}
self.inputImage = loadedImage
}
}
Spacer().frame(height: 0)
}
}
}
}
.navigationBarTitle(contact != nil ? Text("Chat Background") : Text("Default Background"))
.onChange(of:inputImage) { _ in
MLImageManager.sharedInstance().saveBackgroundImageData(inputImage?.pngData(), for:self.contact?.obj)
}
}
}
struct BackgroundSettings_Previews: PreviewProvider {
static var previews: some View {
BackgroundSettings(contact:nil)
}
}

View file

@ -0,0 +1,119 @@
//
// BlockedUsers.swift
// Monal
//
// Created by lissine on 10/9/2024.
// Copyright © 2024 monal-im.org. All rights reserved.
//
struct BlockedUsers: View {
let xmppAccount: xmpp
static private let jidPattern = "^([^@]+@)?[^/\\n]+(\\..{2,})?(/.+)?$"
@State private var blockedJids: [String] = []
@State private var jidToBlock = ""
@State private var showAddingToBlocklistForm = false
@State private var showBlockingUnsupportedPlaceholder = false
@State private var showInvalidJidAlert = false
@StateObject private var overlay = LoadingOverlayState()
private var blockingUnsupported: Bool {
return !xmppAccount.connectionProperties.serverDiscoFeatures.contains("urn:xmpp:blocking")
}
private func reloadBlocksFromDB() {
self.blockedJids = DataLayer.sharedInstance().blockedJids(forAccount: xmppAccount.accountID)
}
var body: some View {
if showBlockingUnsupportedPlaceholder {
ContentUnavailableShimView("Blocking unsupported", systemImage: "iphone.homebutton.slash", description: Text("Your server does not support blocking (XEP-0191)."))
} else {
List {
ForEach(blockedJids, id: \.self) { blockedJid in
Text(blockedJid)
}
.onDelete { indexSet in
for row in indexSet {
showLoadingOverlay(overlay, headlineView: Text("Saving changes to server"), descriptionView: Text(""))
// unblock the jid
MLXMPPManager.sharedInstance().block(false, fullJid: self.blockedJids[row], onAccount: self.xmppAccount.accountID)
}
}
}
.listStyle(.plain)
.navigationTitle("Blocked Users")
.animation(.default, value: blockedJids)
.onAppear {
if !(xmppAccount.accountState.rawValue >= xmppState.stateBound.rawValue && xmppAccount.connectionProperties.accountDiscoDone) {
showLoadingOverlay(overlay, headlineView: Text("Account is connecting..."), descriptionView: Text(""))
}
showBlockingUnsupportedPlaceholder = blockingUnsupported
reloadBlocksFromDB()
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalAccountDiscoDone")).receive(on: RunLoop.main)) { notification in
guard let notificationAccountID = notification.userInfo?["accountID"] as? NSNumber,
notificationAccountID.intValue == xmppAccount.accountID.intValue else {
return
}
// recompute this state variable, so the view is re-rendered if it changed.
showBlockingUnsupportedPlaceholder = blockingUnsupported
reloadBlocksFromDB()
hideLoadingOverlay(overlay)
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalBlockListRefresh")).receive(on: RunLoop.main)) { notification in
guard let notificationAccountID = notification.userInfo?["accountID"] as? NSNumber,
notificationAccountID.intValue == xmppAccount.accountID.intValue else {
return
}
DispatchQueue.main.async {
reloadBlocksFromDB()
DDLogVerbose("Got block list update from account \(xmppAccount)...")
hideLoadingOverlay(overlay)
}
}
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
showAddingToBlocklistForm = true
}, label: {
Image(systemName: "plus")
})
}
}
.alert("Enter the jid that you want to block", isPresented: $showAddingToBlocklistForm, actions: {
TextField("user@example.org/resource", text: $jidToBlock)
.textInputAutocapitalization(.never)
.keyboardType(.emailAddress)
.autocorrectionDisabled()
Button("Block", role: .destructive) {
guard (jidToBlock.range(of: BlockedUsers.jidPattern, options: .regularExpression) != nil) else {
showInvalidJidAlert = true
return
}
showLoadingOverlay(overlay, headlineView: Text("Saving changes to server"), descriptionView: Text(""))
// block the jid
MLXMPPManager.sharedInstance().block(true, fullJid: jidToBlock, onAccount: self.xmppAccount.accountID)
}
Button("Cancel", role: .cancel, action: {})
}
)
// If .onDisappear is applied to the alert or any of its subviews, its perform action won't
// get executed until the whole Blocked Users view is dismissed. Therefore .onChange is used instead
.onChange(of: showAddingToBlocklistForm) { _ in
if !showAddingToBlocklistForm {
// The alert has been dismissed
jidToBlock = ""
}
}
.alert("Input is not a valid jid", isPresented: $showInvalidJidAlert, actions: {})
.addLoadingOverlay(overlay)
}
}
}

View file

@ -0,0 +1,254 @@
//
// BoardingCards.swift
// Monal
//
// Created by Vaidik Dubey on 05/06/24.
// Copyright © 2024 monal-im.org. All rights reserved.
//
import FrameUp
class OnboardingState: ObservableObject {
@defaultsDB("hasCompletedOnboarding")
var hasCompletedOnboarding: Bool
}
struct OnboardingCard: Identifiable {
let id = UUID()
let title: Text?
let description: Text?
let imageName: String?
let articleText: Text?
let customView: AnyView?
let nextText: String?
}
struct OnboardingView: View {
var delegate: SheetDismisserProtocol
let cards: [OnboardingCard]
@ObservedObject var onboardingState = OnboardingState()
@State private var currentIndex = 0
var body: some View {
ZStack {
/// Ensure the ZStack takes the entire area
Color.clear
ForEach(Array(zip(cards, cards.indices)), id: \.1) { card, index in
/// Only show card that's visible
if index == currentIndex {
GeometryReader { proxy in
SmartScrollView(.vertical, showsIndicators: true, optionalScrolling: true, shrinkToFit: false) {
VStack(alignment: .leading, spacing: 16) {
if currentIndex > 0 {
Button {
currentIndex -= 1
} label: {
Label("Back", systemImage: "chevron.left")
.labelStyle(.iconOnly)
.padding(10)
}
} else {
//make sure the space the "back" label will take, is already reserved to not have "jumps" when pressing next
Text("").padding(10)
}
HStack {
if let imageName = card.imageName {
Image(systemName: imageName)
.font(.custom("MarkerFelt-Wide", size: 80))
.foregroundColor(.accentColor)
.accessibilityHidden(true)
}
card.title?
.font(.title)
.fontWeight(.bold)
.foregroundColor(.primary)
.padding(.bottom, 4)
/// This ensures text doesn't get truncated which sometimes happens in ScrollView
.fixedSize(horizontal: false, vertical: true)
}
.accessibilityElement(children: .combine)
.accessibilityAddTraits(.isHeader)
if let description = card.description {
description
.font(.custom("HelveticaNeue-Medium", size: 20))
.foregroundColor(.primary)
.multilineTextAlignment(.leading)
/// This ensures text doesn't get truncated which sometimes happens in ScrollView
.fixedSize(horizontal: false, vertical: true)
}
if card.imageName != nil || card.description != nil || card.imageName != nil {
Spacer().frame(height: 1)
Divider()
Spacer().frame(height: 1)
}
card.articleText?
.font(.custom("HelveticaNeue-Medium", size: 20))
.foregroundColor(.primary)
.multilineTextAlignment(.leading)
card.customView
Spacer()
Group {
if index < cards.count - 1 {
Button {
currentIndex += 1
} label: {
HStack {
Text(card.nextText ?? NSLocalizedString("Next", comment:"onboarding"))
.fontWeight(.bold)
Image(systemName: "chevron.right")
}
}
} else {
Button {
onboardingState.hasCompletedOnboarding = true
delegate.dismissWithoutAnimation()
} label: {
Text(card.nextText ?? NSLocalizedString("Close", comment:"onboarding"))
}
.buttonStyle(MonalProminentButtonStyle())
}
}
.frame(maxWidth: .infinity, alignment: .trailing)
}
.padding(.bottom, 16)
.padding()
/// Sets the minimum frame height to the available height of the scrollview and the maxHeight to infinity
.frame(minHeight: proxy.size.height, maxHeight: .infinity)
}
}
.accessibilityAddTraits(.isModal)
}
}
}
.onAppear {
if UIDevice.current.userInterfaceIdiom != .pad {
//force portrait mode and lock ui there
UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
(UIApplication.shared.delegate as! MonalAppDelegate).orientationLock = .portrait
}
}
}
}
@ViewBuilder
func createOnboardingView(delegate: SheetDismisserProtocol) -> some View {
#if IS_QUICKSY
let cards = [
OnboardingCard(
title: Text("Welcome to Quicksy !"),
description: nil,
imageName: "hand.wave",
articleText: Text("""
Quicksy syncs your contact list in regular intervals to make suggestions about possible contacts who are already on Quicksy.
Quicksy shares and stores images, audio recordings, videos and other media to deliver them to the intended recipients. Files will be stored for up to 30 days.
Find more Information in our [Privacy Policy](https://quicksy.im/privacy.htm).
"""),
customView: nil,
nextText: "Accept and continue"
),
]
#else
let cards = [
OnboardingCard(
title: Text("Welcome to Monal !"),
description: Text("Become part of a worldwide decentralized chat network!"),
imageName: "hand.wave",
articleText: Text("""
Modern iOS and macOS XMPP chat client.\n\nXMPP is a federated network: Just like email, you can register your account on many servers and still talk to anyone, even if they signed up on a different server.\n\nUsing Monal instead of a centralized chat app therefore increases your digital sovereignty.
"""),
customView: nil,
nextText: nil
),
OnboardingCard(
title: Text("Features"),
description: nil,
imageName: "sparkles",
articleText: Text("""
🛜 Decentralized Network :
Leverages the decentralized nature of XMPP, avoiding central servers and increasing your digital sovereignty.
🌐 Data privacy :
We do not sell or track information for external parties (nor for anyone else).
🔐 End-to-end encryption :
Secure multi-end messaging using the OMEMO protocol.
👨💻 Open Source :
The app's source code is publicly available for audit and contribution.
"""),
customView: nil,
nextText: nil
),
OnboardingCard(
title: Text("Settings"),
description: Text("These are important privacy settings you may want to review!"),
imageName: "gear",
articleText: nil,
customView: AnyView(PrivacySettingsSubview(onboardingPart:0)),
nextText: nil
),
OnboardingCard(
title: Text("Settings"),
description: Text("These are important privacy settings you may want to review!"),
imageName: "gear",
articleText: nil,
customView: AnyView(PrivacySettingsSubview(onboardingPart:1)),
nextText: nil
),
OnboardingCard(
title: Text("Even more to customize!"),
description: Text("You can customize even more, just use the button below to open the settings."),
imageName: "hand.wave",
articleText: nil,
customView: AnyView(TakeMeToSettingsView(delegate:delegate)),
nextText: nil
),
]
#endif
OnboardingView(delegate: delegate, cards: cards)
}
struct TakeMeToSettingsView: View {
@ObservedObject var onboardingState = OnboardingState()
var delegate: SheetDismisserProtocol
var body: some View {
HStack {
Spacer()
Button(action: {
let appDelegate = UIApplication.shared.delegate as! MonalAppDelegate
if let activeChats = appDelegate.activeChats {
activeChats.prependGeneralSettings()
}
onboardingState.hasCompletedOnboarding = true
delegate.dismissWithoutAnimation()
}) {
Text("Take me to settings")
}
.buttonStyle(MonalProminentButtonStyle())
Spacer()
}
}
}
struct OnboardingView_Previews: PreviewProvider {
static var delegate = SheetDismisserProtocol()
static var previews: some View {
createOnboardingView(delegate: delegate)
.environmentObject(OnboardingState())
}
}

View file

@ -0,0 +1,77 @@
//
// ChannelMemberList.swift
// Monal
//
// Created by Friedrich Altheide on 17.02.24.
// Copyright © 2024 monal-im.org. All rights reserved.
//
import OrderedCollections
struct ChannelMemberList: View {
private let account: xmpp
@State private var ownAffiliation: String;
@StateObject var channel: ObservableKVOWrapper<MLContact>
@State private var participants: OrderedDictionary<String, String>
init(mucContact: ObservableKVOWrapper<MLContact>) {
account = mucContact.obj.account! as xmpp
_channel = StateObject(wrappedValue:mucContact)
_ownAffiliation = State(wrappedValue:kMucAffiliationNone)
_participants = State(wrappedValue:OrderedDictionary<String, String>())
}
func updateParticipantList() {
ownAffiliation = DataLayer.sharedInstance().getOwnAffiliation(inGroupOrChannel:channel.obj) ?? kMucAffiliationNone
participants.removeAll(keepingCapacity:true)
for memberInfo in Array(DataLayer.sharedInstance().getMembersAndParticipants(ofMuc:channel.contactJid, forAccountID:account.accountID)) {
//ignore ourselves
if let jid = memberInfo["participant_jid"] as? String ?? memberInfo["member_jid"] as? String {
if jid == account.connectionProperties.identity.jid {
continue
}
}
if let nick = memberInfo["room_nick"] as? String {
participants[nick] = memberInfo["affiliation"] as? String ?? kMucAffiliationNone
}
}
participants.sort {
(mucAffiliationToInt($0.value), $0.key.lowercased()) < (mucAffiliationToInt($1.value), $1.key.lowercased())
}
}
var body: some View {
List {
Section(header: Text("\(self.channel.contactDisplayName as String) (affiliation: \(mucAffiliationToString(ownAffiliation)))")) {
ForEach(participants.keys, id: \.self) { participant_key in
ZStack(alignment: .topLeading) {
HStack(alignment: .center) {
Text(participant_key)
Spacer()
Text(mucAffiliationToString(participants[participant_key]))
}
}
}
}
}
.navigationBarTitle(Text("Channel Participants"), displayMode: .inline)
.onAppear {
updateParticipantList()
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalMucParticipantsAndMembersUpdated")).receive(on: RunLoop.main)) { notification in
if let xmppAccount = notification.object as? xmpp, let contact = notification.userInfo?["contact"] as? MLContact {
DDLogVerbose("Got muc participants/members update from account \(xmppAccount)...")
if contact == channel {
updateParticipantList()
}
}
}
}
}
struct ChannelMemberList_Previews: PreviewProvider {
static var previews: some View {
ChannelMemberList(mucContact:ObservableKVOWrapper<MLContact>(MLContact.makeDummyContact(3)));
}
}

View file

@ -0,0 +1,29 @@
//
// ChatPlaceholder.swift
// Monal
//
// Created by Thilo Molitor on 30.11.22.
// Copyright © 2022 monal-im.org. All rights reserved.
//
struct ChatPlaceholder: View {
@Environment(\.colorScheme) var colorScheme
var body: some View {
ZStack {
if colorScheme == .dark {
Color.black
} else {
Color.white
}
Image(colorScheme == .dark ? "park_white_black" : "park_colors")
.resizable()
.scaledToFill()
}
}
}
struct ChatPlaceholder_Previews: PreviewProvider {
static var previews: some View {
ChatPlaceholder()
}
}

View file

@ -0,0 +1,701 @@
//
// ContactDetails.swift
// Monal
//
// Created by Jan on 22.10.21.
// Copyright © 2021 Monal.im. All rights reserved.
//
struct ContactDetails: View {
@Environment(\.presentationMode) private var presentationMode
@State private var ownRole = kMucRoleParticipant
@State private var ownAffiliation = kMucAffiliationNone
@StateObject var contact: ObservableKVOWrapper<MLContact>
@State private var showingRemoveAvatarConfirmation = false
@State private var showingBlockContactConfirmation = false
@State private var showingCannotBlockAlert = false
@State private var showingRemoveContactConfirmation = false
@State private var showingAddContactConfirmation = false
@State private var showingClearHistoryConfirmation = false
@State private var showingResetOmemoSessionConfirmation = false
@State private var showingCannotEncryptAlert = false
@State private var showingShouldDisableEncryptionAlert = false
@State private var isEditingNickname = false
@State private var inputImage: UIImage?
@State private var showingImagePicker = false
@State private var showingSheetEditSubject = false
@State private var showingDestroyConfirmation = false
@State private var alertPrompt = AlertPrompt(dismissLabel: Text("Close"))
@State private var showAlert = false
@State private var success = false
@State private var successCallback: monal_void_block_t?
@StateObject private var overlay = LoadingOverlayState()
var delegate: SheetDismisserProtocol?
private var account: xmpp
init(delegate: SheetDismisserProtocol?, contact: ObservableKVOWrapper<MLContact>) {
self.delegate = delegate
_contact = StateObject(wrappedValue: contact)
self.account = contact.obj.account!
}
private func updateRoleAndAffiliation() {
if contact.isMuc {
self.ownRole = DataLayer.sharedInstance().getOwnRole(inGroupOrChannel: contact.obj) ?? kMucRoleNone
self.ownAffiliation = DataLayer.sharedInstance().getOwnAffiliation(inGroupOrChannel:contact.obj) ?? kMucAffiliationNone
} else {
self.ownRole = kMucRoleParticipant
self.ownAffiliation = kMucAffiliationNone
}
}
private func errorAlert(title: Text, message: Text = Text("")) {
alertPrompt.title = title
alertPrompt.message = message
showAlert = true
}
private func successAlert(title: Text, message: Text = Text("")) {
alertPrompt.title = title
alertPrompt.message = message
showAlert = true
success = true // < dismiss entire view on close
}
private func showImagePicker() {
#if targetEnvironment(macCatalyst)
let picker = DocumentPickerViewController(
supportedTypes: [UTType.image],
onPick: { url in
if let imageData = try? Data(contentsOf: url) {
if let loadedImage = UIImage(data: imageData) {
self.inputImage = loadedImage
}
}
},
onDismiss: {
//do nothing on dismiss
}
)
UIApplication.shared.windows.first?.rootViewController?.present(picker, animated: true)
#else
showingImagePicker = true
#endif
}
var body: some View {
Form {
Section {
VStack(spacing: 20) {
if !contact.isSelfChat {
Image(uiImage: contact.avatar)
.resizable()
.scaledToFit()
.applyClosure {view in
if contact.isMuc {
if ownAffiliation == kMucAffiliationOwner {
view.accessibilityLabel((contact.mucType == kMucTypeGroup) ? Text("Change Group Avatar") : Text("Change Channel Avatar"))
.onTapGesture {
showImagePicker()
}
.addTopRight {
if contact.hasAvatar {
Button(action: {
showingRemoveAvatarConfirmation = true
}, label: {
Image(systemName: "xmark.circle.fill")
.resizable()
.frame(width: 24.0, height: 24.0)
.accessibilityLabel((contact.mucType == kMucTypeGroup) ? Text("Remove Group Avatar") : Text("Remove Channel Avatar"))
.symbolRenderingMode(.palette)
.foregroundStyle(.white, .red)
})
.buttonStyle(.borderless)
.offset(x: 8, y: -8)
} else {
Button(action: {
showImagePicker()
}, label: {
Image(systemName: "pencil.circle.fill")
.resizable()
.frame(width: 24.0, height: 24.0)
.accessibilityLabel((contact.mucType == kMucTypeGroup) ? Text("Change Group Avatar") : Text("Change Channel Avatar"))
})
.buttonStyle(.borderless)
.offset(x: 8, y: -8)
}
}
} else {
view.accessibilityLabel((contact.mucType == kMucTypeGroup) ? Text("Group Avatar") : Text("Channel Avatar"))
}
} else {
view.accessibilityLabel(Text("Avatar"))
}
}
.frame(width: 150, height: 150, alignment: .center)
.shadow(radius: 7)
.actionSheet(isPresented: $showingRemoveAvatarConfirmation) {
ActionSheet(
title: Text("Really remove avatar?"),
message: Text("This will remove the current avatar image and revert this group/channel to the default one."),
buttons: [
.cancel(),
.destructive(
Text("Yes"),
action: {
showPromisingLoadingOverlay(overlay, headlineView:Text("Removing avatar..."), descriptionView:Text("")) {
promisifyMucAction(account:account, mucJid:contact.contactJid) {
self.account.mucProcessor.publishAvatar(nil, forMuc: contact.contactJid)
}
}.catch { error in
errorAlert(title: Text("Error removing avatar!"), message: Text("\(String(describing:error))"))
hideLoadingOverlay(overlay)
}
}
)
]
)
}
}
Button {
UIPasteboard.general.setValue(contact.contactJid as String, forPasteboardType:UTType.utf8PlainText.identifier as String)
UIAccessibility.post(notification: .announcement, argument: "JID Copied")
} label: {
HStack {
Text(contact.contactJid as String)
Image(systemName: "doc.on.doc")
.foregroundColor(.primary)
.accessibilityHidden(true)
}
.accessibilityHint("Copies JID")
}
.buttonStyle(.borderless)
// //TODO: wait for account edit to become swiftui
// if contact.isSelfChat {
// Button {
// //TODO: open account edit
// } label: {
// Text("Open account settings")
// .accessibilityHint("Open account settings")
// }
// .buttonStyle(.borderless)
// }
//only show account jid if more than one is configured
if MLXMPPManager.sharedInstance().connectedXMPP.count > 1 && !contact.isSelfChat {
Text("Account: \(account.connectionProperties.identity.jid)")
}
if !contact.isSelfChat && !contact.isMuc {
if let lastInteractionTime = contact.lastInteractionTime as Date? {
if lastInteractionTime.timeIntervalSince1970 > 0 {
Text(String(format: NSLocalizedString("Last seen: %@", comment: ""),
DateFormatter.localizedString(from: lastInteractionTime, dateStyle: DateFormatter.Style.short, timeStyle: DateFormatter.Style.short)))
} else {
Text(String(format: NSLocalizedString("Last seen: %@", comment: ""), NSLocalizedString("now", comment: "")))
}
} else {
Text(String(format: NSLocalizedString("Last seen: %@", comment: ""), NSLocalizedString("unknown", comment: "")))
}
}
if !contact.isMuc, let statusMessage = contact.statusMessage as String?, statusMessage.count > 0 {
VStack {
Text("Status message:")
Text(contact.statusMessage as String)
.fixedSize(horizontal: false, vertical: true)
}
}
if contact.isMuc && ((contact.groupSubject as String).count > 0 || ownRole == kMucRoleModerator) {
VStack {
if ownRole == kMucRoleModerator {
Button {
showingSheetEditSubject.toggle()
} label: {
if contact.obj.mucType == kMucTypeGroup {
HStack {
Text("Group subject:")
Spacer().frame(width:8)
Image(systemName: "pencil")
.foregroundColor(.primary)
.accessibilityHidden(true)
}
.accessibilityHint("Edit Group Subject")
} else {
HStack {
Text("Channel subject:")
Spacer().frame(width:8)
Image(systemName: "pencil")
.foregroundColor(.primary)
.accessibilityHidden(true)
}
.accessibilityHint("Edit Channel Subject")
}
}
.buttonStyle(.borderless)
} else {
Text("Group subject:")
}
Text(contact.groupSubject as String)
.fixedSize(horizontal: false, vertical: true)
}
}
}
.foregroundColor(.primary)
.padding([.top, .bottom])
.frame(maxWidth: .infinity)
}
// info/nondestructive buttons
Section {
if !contact.isSelfChat {
Button {
if contact.isMuc {
if !contact.isMuted && !contact.isMentionOnly {
contact.obj.toggleMentionOnly(true)
} else if !contact.isMuted && contact.isMentionOnly {
contact.obj.toggleMentionOnly(false)
contact.obj.toggleMute(true)
} else {
contact.obj.toggleMentionOnly(false)
contact.obj.toggleMute(false)
}
} else {
contact.obj.toggleMute(!contact.isMuted)
}
} label: {
if contact.isMuted {
Label {
contact.isMuc ? Text("Notifications disabled") : Text("Contact is muted")
} icon: {
Image(systemName: "bell.slash.fill")
}
.foregroundStyle(Color.red)
} else if contact.isMuc && contact.isMentionOnly {
Label {
Text("Notify only when mentioned")
} icon: {
Image(systemName: "bell.badge")
}
.foregroundStyle(Color.primary)
} else {
Label {
contact.isMuc ? Text("Notify on all messages") : Text("Contact is not muted")
} icon: {
Image(systemName: "bell.fill")
}
.foregroundStyle(Color.green)
}
}
}
#if !DISABLE_OMEMO
if (!contact.isMuc || (contact.isMuc && contact.mucType == kMucTypeGroup)) && !HelperTools.isContactBlacklistedForEncryption(contact.obj) {
Button {
if contact.isEncrypted {
showingShouldDisableEncryptionAlert = true
} else {
showingCannotEncryptAlert = !contact.obj.toggleEncryption(!contact.isEncrypted)
}
} label: {
if contact.isEncrypted {
Label {
Text("Messages are encrypted")
} icon: {
Image(systemName: "lock.fill")
}
.foregroundStyle(Color.green)
} else {
Label {
Text("Messages are NOT encrypted")
} icon: {
Image(systemName: "lock.open.fill")
}
.foregroundStyle(Color.red)
}
}
.alert(isPresented: $showingCannotEncryptAlert) {
Alert(title: Text("Encryption Not Supported"), message: Text("This contact does not appear to have any devices that support encryption, please try again later if you think this is wrong."), dismissButton: .default(Text("Close")))
}
.actionSheet(isPresented: $showingShouldDisableEncryptionAlert) {
ActionSheet(
title: Text("Disable encryption?"),
message: Text("Do you really want to disable encryption for this contact?"),
buttons: [
.cancel(
Text("No, keep encryption activated"),
action: { }
),
.destructive(
Text("Yes, deactivate encryption"),
action: {
showingCannotEncryptAlert = !contact.obj.toggleEncryption(!contact.isEncrypted)
}
)
]
)
}
//.buttonStyle(BorderlessButtonStyle())
}
#endif
if contact.isMuc && ownAffiliation == kMucAffiliationOwner {
let label = contact.obj.mucType == kMucTypeGroup ? NSLocalizedString("Rename Group", comment:"") : NSLocalizedString("Rename Channel", comment:"")
TextField(label, text: $contact.fullNameView, onEditingChanged: {
isEditingNickname = $0
})
.accessibilityLabel(contact.obj.mucType == kMucTypeGroup ? Text("Group name") : Text("Channel name"))
.addClearButton(isEditing: isEditingNickname, text: $contact.fullNameView)
} else if !contact.isMuc && !contact.isSelfChat {
TextField(NSLocalizedString("Rename Contact", comment: "placeholder text in contact details"), text: $contact.nickNameView, onEditingChanged: {
isEditingNickname = $0
})
.accessibilityLabel(Text("Nickname"))
.addClearButton(isEditing: isEditingNickname, text: $contact.nickNameView)
}
Toggle(isOn: Binding(get: {
contact.isPinned
}, set: {
contact.obj.togglePinnedChat($0)
})) {
Text("Pin Chat")
}
#if !DISABLE_OMEMO
if !HelperTools.isContactBlacklistedForEncryption(contact.obj) && !contact.isSelfChat {
if !contact.isMuc || contact.mucType == kMucTypeGroup {
NavigationLink(destination: LazyClosureView(OmemoKeysView(omemoKeys: OmemoKeysForChat(viewContact: contact)))) {
Text("Encryption Keys")
}
}
}
#endif
if !contact.isMuc && !contact.isSelfChat {
NavigationLink(destination: LazyClosureView(ContactResources(contact: contact))) {
Text("Resources")
}
}
let accountJid = account.connectionProperties.identity.jid
let displayName = contact.contactDisplayName as String
let sharedUrl = HelperTools.getSharedDocumentsURL(forPathComponents:[accountJid, displayName])
if UIApplication.shared.canOpenURL(sharedUrl) && FileManager.default.fileExists(atPath:sharedUrl.path) {
NavigationLink(destination: LazyClosureView{MediaGalleryView(contact: contact.contactJid as String, accountID: contact.accountID)}) {
Text("Shared Media")
}
Button(action: {
UIApplication.shared.open(sharedUrl, options:[:])
}) {
Text("Shared Files")
}
}
NavigationLink(destination: LazyClosureView(BackgroundSettings(contact:contact))) {
Text("Change Chat Background")
}
if contact.obj.isMuc && contact.obj.mucType == kMucTypeGroup {
NavigationLink(destination: LazyClosureView(MemberList(mucContact:contact))) {
Text("Group Members")
}
} else if contact.obj.isMuc && contact.obj.mucType == kMucTypeChannel {
if [kMucAffiliationOwner, kMucAffiliationAdmin].contains(ownAffiliation) {
NavigationLink(destination: LazyClosureView(MemberList(mucContact:contact))) {
Text("Channel Participants")
}
} else {
NavigationLink(destination: LazyClosureView(ChannelMemberList(mucContact:contact))) {
Text("Channel Participants")
}
}
}
}
.listStyle(.plain)
Section { // the destructive section...
if !contact.isSelfChat {
Button(action: {
if !contact.isBlocked {
showingBlockContactConfirmation = true
} else {
showingCannotBlockAlert = !contact.obj.toggleBlocked(!contact.isBlocked)
}
}) {
if !contact.isBlocked {
Text("Block Contact")
.foregroundColor(.red)
} else {
Text("Unblock Contact")
}
}
.alert(isPresented: $showingCannotBlockAlert) {
Alert(title: Text("Blocking/Unblocking Not Supported"), message: Text("The server does not support blocking (XEP-0191)."), dismissButton: .default(Text("Close")))
}
.actionSheet(isPresented: $showingBlockContactConfirmation) {
ActionSheet(
title: Text("Block Contact"),
message: Text("Do you really want to block this contact? You won't receive any messages from this contact."),
buttons: [
.cancel(),
.destructive(
Text("Yes"),
action: {
showingCannotBlockAlert = !contact.obj.toggleBlocked(!contact.isBlocked)
}
)
]
)
}
Group {
if contact.isInRoster {
Button(action: {
showingRemoveContactConfirmation = true
}) {
if contact.isMuc {
if contact.mucType == kMucTypeGroup {
Text("Leave Group")
.foregroundColor(.red)
} else {
Text("Leave Channel")
.foregroundColor(.red)
}
} else {
Text("Remove from contacts")
.foregroundColor(.red)
}
}
.actionSheet(isPresented: $showingRemoveContactConfirmation) {
ActionSheet(
title: Text(contact.isMuc ? NSLocalizedString("Leave this conversation", comment: "") : String(format: NSLocalizedString("Remove %@ from contacts?", comment: ""), contact.contactJid)),
message: Text(contact.isMuc ? NSLocalizedString("You will no longer receive messages from this conversation", comment: "") : NSLocalizedString("They will no longer see when you are online. They may not be able to send you encrypted messages.", comment: "")),
buttons: [
.cancel(),
.destructive(
Text("Yes"),
action: {
contact.obj.removeFromRoster() //this will dismiss the chatview via kMonalContactRemoved notification
//NOTE: since we can get opened from objc through active chats,
//NOTE: we still need to support our SheetDismisserProtocol
if let delegate = self.delegate {
delegate.dismiss()
} else {
self.presentationMode.wrappedValue.dismiss()
}
}
)
]
)
}
} else {
Button(action: {
showingAddContactConfirmation = true
}) {
if contact.isMuc {
if contact.mucType == kMucTypeGroup {
Text("Join Group")
} else {
Text("Join Channel")
}
} else {
Text("Add to contacts")
}
}
.actionSheet(isPresented: $showingAddContactConfirmation) {
ActionSheet(
title: Text(contact.isMuc ? (contact.mucType == kMucTypeGroup ? NSLocalizedString("Join Group", comment: "") : NSLocalizedString("Join Channel", comment: "")) : String(format: NSLocalizedString("Add %@ to your contacts?", comment: ""), contact.contactJid)),
message: Text(contact.isMuc ? NSLocalizedString("You will receive subsequent messages from this conversation", comment: "") : NSLocalizedString("They will see when you are online. They will be able to send you encrypted messages.", comment: "")),
buttons: [
.cancel(),
.default(
Text("Yes"),
action: {
contact.obj.addToRoster()
}
),
]
)
}
}
}
}
if ownAffiliation == kMucAffiliationOwner {
Section {
Button(action: {
showingDestroyConfirmation = true
}) {
if contact.mucType == kMucTypeGroup {
Text("Destroy Group").foregroundColor(.red)
} else {
Text("Destroy Channel").foregroundColor(.red)
}
}
.actionSheet(isPresented: $showingDestroyConfirmation) {
ActionSheet(
title: contact.mucType == kMucTypeGroup ? Text("Destroy Group") : Text("Destroy Channel"),
message: contact.mucType == kMucTypeGroup ? Text("Do you really want to destroy this group? Every member will be kicked out and it will be destroyed afterwards.") : Text("Do you really want to destroy this channel? Every member will be kicked out and it will be destroyed afterwards."),
buttons: [
.cancel(),
.destructive(
Text("Yes"),
action: {
showPromisingLoadingOverlay(overlay, headlineView:contact.mucType == kMucTypeGroup ? Text("Destroying group...") : Text("Destroying channel..."), descriptionView:Text("")) {
promisifyMucAction(account:account, mucJid:contact.contactJid) {
self.account.mucProcessor.destroyRoom(contact.contactJid as String)
}
}.done { callback in
if let callback = callback {
self.successCallback = callback
}
successAlert(title: Text("Success"), message: contact.mucType == kMucTypeGroup ? Text("Successfully destroyed group.") : Text("Successfully destroyed channel."))
}.catch { error in
errorAlert(title: Text("Error destroying group!"), message: Text("\(String(describing:error))"))
}
}
)
]
)
}
}
}
Button(action: {
showingClearHistoryConfirmation = true
}) {
if contact.isMuc {
if contact.obj.mucType == kMucTypeGroup {
Text("Clear chat history of this group")
} else {
Text("Clear chat history of this channel")
}
} else {
Text("Clear chat history of this contact")
}
}
.foregroundColor(.red)
.actionSheet(isPresented: $showingClearHistoryConfirmation) {
ActionSheet(
title: Text("Clear History"),
message: Text("Do you really want to clear all messages exchanged in this conversation? If using OMEMO you won't even be able to load them from your server again."),
buttons: [
.cancel(),
.destructive(
Text("Yes"),
action: {
contact.obj.clearHistory()
}
)
]
)
}
}
#if !DISABLE_OMEMO
//omemo debug stuff, should be removed in a few months
Section {
// only display omemo session reset button on 1:1 and private groups
if contact.obj.isMuc == false || (contact.isMuc && contact.mucType == kMucTypeGroup) {
Button(action: {
showingResetOmemoSessionConfirmation = true
}) {
Text("Reset OMEMO session")
.foregroundColor(.red)
}
.actionSheet(isPresented: $showingResetOmemoSessionConfirmation) {
ActionSheet(
title: Text("Reset OMEMO session"),
message: Text("Do you really want to reset the OMEMO session? You should only reset the connection if you know what you are doing!"),
buttons: [
.cancel(),
.destructive(
Text("Yes"),
action: {
self.account.omemo.clearAllSessions(forJid:contact.contactJid);
}
)
]
)
}
}
}
#endif
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.tint(Color.primary)
.addLoadingOverlay(overlay)
.navigationBarTitle(contact.contactDisplayName as String, displayMode:.inline)
.alert(isPresented: $showAlert) {
Alert(title: alertPrompt.title, message: alertPrompt.message, dismissButton:.default(Text("Close"), action: {
showAlert = false
if self.success == true {
if let callback = self.successCallback {
callback()
}
//close muc ui and leave chat ui of this muc
if let activeChats = (UIApplication.shared.delegate as! MonalAppDelegate).activeChats {
activeChats.presentChat(with:nil)
}
}
}))
}
.sheet(isPresented: $showingSheetEditSubject) {
LazyClosureView(EditGroupSubject(contact: contact))
}
.sheet(isPresented:$showingImagePicker) {
ImagePicker(image:$inputImage)
}
.sheet(isPresented: $inputImage.optionalMappedToBool()) {
ImageCropView(originalImage: inputImage!, configureBlock: { cropViewController in
cropViewController.aspectRatioPreset = .presetSquare
cropViewController.aspectRatioLockEnabled = true
cropViewController.aspectRatioPickerButtonHidden = true
cropViewController.resetAspectRatioEnabled = false
}, onCanceled: {
inputImage = nil
}) { (image, cropRect, angle) in
showPromisingLoadingOverlay(overlay, headlineView:Text("Uploading avatar..."), descriptionView:Text("")) {
promisifyMucAction(account:account, mucJid:contact.contactJid) {
self.account.mucProcessor.publishAvatar(image, forMuc: contact.contactJid)
}
}.catch { error in
errorAlert(title: Text("Error changing avatar!"), message: Text("\(String(describing:error))"))
hideLoadingOverlay(overlay)
}
}
}
.onChange(of:contact.avatar as UIImage) { _ in
hideLoadingOverlay(overlay)
}
.onAppear {
self.updateRoleAndAffiliation()
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalMucParticipantsAndMembersUpdated")).receive(on: RunLoop.main)) { notification in
if let xmppAccount = notification.object as? xmpp, let notificationContact = notification.userInfo?["contact"] as? MLContact {
DDLogVerbose("Got muc participants/members update from account \(xmppAccount)...")
if notificationContact == contact {
self.updateRoleAndAffiliation()
}
}
}
}
}
struct ContactDetails_Previews: PreviewProvider {
static var previews: some View {
ContactDetails(delegate:nil, contact:ObservableKVOWrapper<MLContact>(MLContact.makeDummyContact(0)))
ContactDetails(delegate:nil, contact:ObservableKVOWrapper<MLContact>(MLContact.makeDummyContact(1)))
ContactDetails(delegate:nil, contact:ObservableKVOWrapper<MLContact>(MLContact.makeDummyContact(2)))
ContactDetails(delegate:nil, contact:ObservableKVOWrapper<MLContact>(MLContact.makeDummyContact(3)))
ContactDetails(delegate:nil, contact:ObservableKVOWrapper<MLContact>(MLContact.makeDummyContact(4)))
}
}

View file

@ -0,0 +1,86 @@
//
// ContactEntry.swift
// Monal
//
// Created by Friedrich Altheide on 28.11.23.
// Copyright © 2023 monal-im.org. All rights reserved.
//
struct ContactEntry<AdditionalContent: View>: View {
let contact: ObservableKVOWrapper<MLContact>
let selfnotesPrefix: Bool
let fallback: String?
@ViewBuilder let additionalContent: () -> AdditionalContent
init(contact:ObservableKVOWrapper<MLContact>, selfnotesPrefix: Bool = true, fallback: String? = nil) where AdditionalContent == EmptyView {
self.init(contact:contact, selfnotesPrefix:selfnotesPrefix, fallback:fallback, additionalContent:{ EmptyView() })
}
init(contact:ObservableKVOWrapper<MLContact>, fallback: String?) where AdditionalContent == EmptyView {
self.init(contact:contact, selfnotesPrefix:true, fallback:fallback, additionalContent:{ EmptyView() })
}
init(contact:ObservableKVOWrapper<MLContact>, @ViewBuilder additionalContent: @escaping () -> AdditionalContent) {
self.init(contact:contact, selfnotesPrefix:true, additionalContent:additionalContent)
}
init(contact:ObservableKVOWrapper<MLContact>, fallback: String?, @ViewBuilder additionalContent: @escaping () -> AdditionalContent) {
self.init(contact:contact, selfnotesPrefix:true, fallback:fallback, additionalContent:additionalContent)
}
init(contact:ObservableKVOWrapper<MLContact>, selfnotesPrefix: Bool, @ViewBuilder additionalContent: @escaping () -> AdditionalContent) {
self.init(contact:contact, selfnotesPrefix:selfnotesPrefix, fallback:nil, additionalContent:additionalContent)
}
init(contact:ObservableKVOWrapper<MLContact>, selfnotesPrefix: Bool, fallback: String?, @ViewBuilder additionalContent: @escaping () -> AdditionalContent) {
self.contact = contact
self.selfnotesPrefix = selfnotesPrefix
self.fallback = fallback
self.additionalContent = additionalContent
}
var body:some View {
ZStack(alignment: .topLeading) {
HStack(alignment: .center) {
Image(uiImage: contact.avatar)
.resizable()
.frame(width: 40, height: 40, alignment: .center)
VStack(alignment: .leading) {
if selfnotesPrefix {
// use the if to make sure this view gets updated if the contact display name changes
// (the condition is never false, because contactDisplayName can not be nil)
if (contact.contactDisplayName as String?) != nil {
Text(contact.obj.contactDisplayName(withFallback:fallback))
}
} else {
// use the if to make sure this view gets updated if the contact display name changes
// (the condition is never false, because contactDisplayNameWithoutSelfnotesPrefix can not be nil)
if (contact.contactDisplayNameWithoutSelfnotesPrefix as String?) != nil {
Text(contact.obj.contactDisplayName(withFallback:fallback, andSelfnotesPrefix:false))
}
}
additionalContent()
Text(contact.contactJid as String)
.foregroundColor(Color(UIColor.secondaryLabel))
.font(.footnote)
}
}
}
}
}
#Preview {
ContactEntry(contact:ObservableKVOWrapper(MLContact.makeDummyContact(0)))
}
#Preview {
ContactEntry(contact:ObservableKVOWrapper(MLContact.makeDummyContact(1)))
}
#Preview {
ContactEntry(contact:ObservableKVOWrapper(MLContact.makeDummyContact(2)))
}
#Preview {
ContactEntry(contact:ObservableKVOWrapper(MLContact.makeDummyContact(3)))
}

View file

@ -0,0 +1,135 @@
//
// ContactList.swift
// Monal
//
// Created by Jan on 15.12.22.
// Copyright © 2022 monal-im.org. All rights reserved.
//
import OrderedCollections
struct ContactPickerEntry: View {
let contact : ObservableKVOWrapper<MLContact>
let isPicked: Bool
let isExistingMember: Bool
var body:some View {
ZStack(alignment: .topLeading) {
HStack(alignment: .center) {
if(isExistingMember) {
Image(systemName: "checkmark.circle")
.foregroundColor(.gray)
} else if(isPicked) {
Image(systemName: "checkmark.circle")
.foregroundColor(.accentColor)
} else {
Image(systemName: "circle")
}
ContactEntry(contact: contact)
}
}
}
}
struct ContactPicker: View {
typealias completionType = (OrderedSet<ObservableKVOWrapper<MLContact>>)->Void
let account: xmpp
@Binding var returnedContacts: OrderedSet<ObservableKVOWrapper<MLContact>>
@State var selectedContacts: OrderedSet<ObservableKVOWrapper<MLContact>>
@State var searchText = ""
@State var isEditingSearchInput = false
let allowRemoval: Bool
let completion: completionType?
init(_ account: xmpp, initializeFrom contacts: OrderedSet<ObservableKVOWrapper<MLContact>>, allowRemoval: Bool = true, completion:completionType?) {
self.account = account
self.allowRemoval = allowRemoval
self.completion = completion
_selectedContacts = State(wrappedValue:OrderedSet())
//use a temporary storage because we don't have a binding to the outside world but use the completion handler
var storage = contacts
_returnedContacts = Binding(
get: { storage },
set: { storage = $0 }
)
buildPreselectedContacts(contacts)
DDLogError("self.allowRemoval = \(String(describing:self.allowRemoval))")
}
init(_ account: xmpp, binding returnedContacts: Binding<OrderedSet<ObservableKVOWrapper<MLContact>>>, allowRemoval: Bool = true) {
self.account = account
self.allowRemoval = allowRemoval
self.completion = nil
_selectedContacts = State(wrappedValue:OrderedSet())
_returnedContacts = returnedContacts
buildPreselectedContacts(returnedContacts.wrappedValue)
}
private mutating func buildPreselectedContacts(_ source: OrderedSet<ObservableKVOWrapper<MLContact>>) {
//build currently selected list of contacts
var contactsTmp: OrderedSet<ObservableKVOWrapper<MLContact>> = OrderedSet()
for contact in source {
contactsTmp.append(contact)
}
_selectedContacts = State(wrappedValue:contactsTmp)
}
private var allContacts: OrderedSet<ObservableKVOWrapper<MLContact>> {
//build list of all possible contacts on this account (excluding selfchat and other mucs)
var contactsTmp: OrderedSet<ObservableKVOWrapper<MLContact>> = OrderedSet()
for contact in DataLayer.sharedInstance().possibleGroupMembers(forAccount: account.accountID) {
contactsTmp.append(ObservableKVOWrapper(contact))
}
return contactsTmp
}
private var searchResults : OrderedSet<ObservableKVOWrapper<MLContact>> {
if searchText.isEmpty {
return self.allContacts
} else {
var filteredContacts: OrderedSet<ObservableKVOWrapper<MLContact>> = OrderedSet()
for contact in self.allContacts {
if (contact.contactDisplayName as String).lowercased().contains(searchText.lowercased()) ||
(contact.contactJid as String).contains(searchText.lowercased()) {
filteredContacts.append(contact)
}
}
return filteredContacts
}
}
var body: some View {
if(allContacts.isEmpty) {
Text("No contacts to show :(")
.navigationTitle("Contact Lists")
} else {
List(searchResults) { contact in
let contactIsSelected = self.selectedContacts.contains(contact);
let contactIsAlreadyMember = self.returnedContacts.contains(contact);
ContactPickerEntry(contact: contact, isPicked: contactIsSelected, isExistingMember: !(!contactIsAlreadyMember || allowRemoval))
.onTapGesture {
// only allow changes to members that are not already part of the group
if(!contactIsAlreadyMember || allowRemoval) {
if(contactIsSelected) {
self.selectedContacts.remove(contact)
} else {
self.selectedContacts.append(contact)
}
}
}
}
.searchable(text: $searchText, placement: .automatic, prompt: nil)
.listStyle(.inset)
.navigationBarTitle(Text("Contact Selection"), displayMode: .inline)
.onDisappear {
returnedContacts.removeAll()
for contact in selectedContacts {
returnedContacts.append(contact)
}
if let completion = completion {
completion(returnedContacts)
}
}
}
}
}

View file

@ -0,0 +1,120 @@
//
// ContactRequestsMenu.swift
// Monal
//
// Created by Jan on 27.10.22.
// Copyright © 2022 monal-im.org. All rights reserved.
//
struct ContactRequestsMenuEntry: View {
let contact : MLContact
@State private var isDeleted = false
var body: some View {
HStack {
Text(contact.contactJid)
.foregroundColor(.secondary)
Spacer()
Group {
Button {
let appDelegate = UIApplication.shared.delegate as! MonalAppDelegate
appDelegate.openChat(of:contact)
} label: {
Image(systemName: "text.bubble")
.foregroundStyle(Color.primary)
}
//see https://www.hackingwithswift.com/forums/swiftui/tap-button-in-hstack-activates-all-button-actions-ios-14-swiftui-2/2952
.buttonStyle(BorderlessButtonStyle())
Button {
// deny request
MLXMPPManager.sharedInstance().remove(contact)
} label: {
Image(systemName: "trash.circle")
.foregroundStyle(Color.red)
}
//see https://www.hackingwithswift.com/forums/swiftui/tap-button-in-hstack-activates-all-button-actions-ios-14-swiftui-2/2952
.buttonStyle(BorderlessButtonStyle())
Button {
// accept request
MLXMPPManager.sharedInstance().add(contact)
let appDelegate = UIApplication.shared.delegate as! MonalAppDelegate
appDelegate.openChat(of:contact)
} label: {
Image(systemName: "checkmark.circle")
.foregroundStyle(Color.green)
}
//see https://www.hackingwithswift.com/forums/swiftui/tap-button-in-hstack-activates-all-button-actions-ios-14-swiftui-2/2952
.buttonStyle(BorderlessButtonStyle())
}
.font(.largeTitle)
}
}
}
struct ContactRequestsMenu: View {
@State var pendingRequests: [xmpp:[MLContact]] = [:]
@State var enabledAccounts: [Int:xmpp] = [:]
func updateRequests() {
let requests = DataLayer.sharedInstance().allContactRequests() as! [MLContact]
enabledAccounts.removeAll()
for account in MLXMPPManager.sharedInstance().connectedXMPP as! [xmpp] {
enabledAccounts[account.accountID.intValue] = account
}
pendingRequests.removeAll()
for contact in requests {
//add only requests having an enabled (dubbed connected) account
//(should be a noop because allContactRequests() returns only enabled accounts)
if let account = enabledAccounts[contact.accountID.intValue] {
if pendingRequests[account] == nil {
pendingRequests[account] = []
}
pendingRequests[account]!.append(contact)
}
}
}
var body: some View {
Section(header: Text("Allowing someone to add you as a contact lets them see your profile picture and when you are online.")) {
if(pendingRequests.isEmpty) {
Text("No pending contact requests")
.foregroundColor(.secondary)
} else {
List {
ForEach(pendingRequests.sorted(by:{ $0.0.connectionProperties.identity.jid < $1.0.connectionProperties.identity.jid }), id: \.key) { account, requests in
if enabledAccounts.count == 1 {
ForEach(requests.indices, id: \.self) { idx in
ContactRequestsMenuEntry(contact: requests[idx])
}
} else {
Section(header: Text("Account: \(account.connectionProperties.identity.jid)")) {
ForEach(requests.indices, id: \.self) { idx in
ContactRequestsMenuEntry(contact: requests[idx])
}
}
}
}
}
}
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalContactRefresh")).receive(on: RunLoop.main)) { notification in
updateRequests()
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalContactRemoved")).receive(on: RunLoop.main)) { notification in
updateRequests()
}
.onAppear {
updateRequests()
}
}
}
struct ContactRequestsMenu_Previews: PreviewProvider {
static var previews: some View {
ContactRequestsMenu()
}
}

View file

@ -0,0 +1,159 @@
//
// ContactResources.swift
// Monal
//
// Created by Friedrich Altheide on 24.12.21.
// Copyright © 2021 Monal.im. All rights reserved.
//
import OrderedCollections
@ViewBuilder
func resourceRowElement(_ k: String, _ v: some View, space: CGFloat = 5) -> some View {
HStack {
Text(k).font(.headline)
Spacer()
v.foregroundColor(.secondary)
}
}
struct ContactResources: View {
@StateObject var contact: ObservableKVOWrapper<MLContact>
@State var contactVersionInfos: [String:ObservableKVOWrapper<MLContactSoftwareVersionInfo>]
@State private var showCaps: String?
init(contact: ObservableKVOWrapper<MLContact>, previewMock: [String:ObservableKVOWrapper<MLContactSoftwareVersionInfo>]? = nil) {
_contact = StateObject(wrappedValue: contact)
if previewMock != nil {
self.contactVersionInfos = previewMock!
} else {
var tmpInfos:[String:ObservableKVOWrapper<MLContactSoftwareVersionInfo>] = [:]
for ressourceName in DataLayer.sharedInstance().resources(for: contact.obj) {
// load already known software version info from database
if let softwareInfo = DataLayer.sharedInstance().getSoftwareVersionInfo(forContact:contact.obj.contactJid, resource:ressourceName, andAccount:contact.obj.accountID) {
tmpInfos[ressourceName] = ObservableKVOWrapper<MLContactSoftwareVersionInfo>(softwareInfo)
}
}
self.contactVersionInfos = tmpInfos
}
}
var body: some View {
List {
ForEach(self.contactVersionInfos.sorted(by:{ $0.0 < $1.0 }), id: \.key) { key, versionInfo in
Section {
VStack {
resourceRowElement("Resource:", Text(versionInfo.resource as String))
resourceRowElement("Client Name:", Text(versionInfo.appName as String? ?? ""))
resourceRowElement("Client Version:", Text(versionInfo.appVersion as String? ?? ""))
resourceRowElement("OS:", Text(versionInfo.platformOs as String? ?? ""))
if let lastInteraction = versionInfo.lastInteraction as Date? {
if lastInteraction.timeIntervalSince1970 == 0 {
resourceRowElement("Last Interaction:", Text("Currently Online"))
} else {
resourceRowElement("Last Interaction:", Text(lastInteraction.formatted(date:.numeric, time:.standard)))
}
} else {
resourceRowElement("Last Interaction:", Text("unsupported"))
}
}
.onTapGesture(count: 2, perform: {
showCaps = versionInfo.resource
})
}
}
}
.richAlert(isPresented:$showCaps, title:Text("XMPP Capabilities")) { resource in
VStack(alignment: .leading) {
Text("The resource '\(resource)' has the following capabilities:")
.font(Font.body.weight(.semibold))
Spacer()
.frame(height: 20)
Section {
let capsVer = DataLayer.sharedInstance().getVerForUser(self.contact.contactJid, andResource:resource, onAccountID:self.contact.accountID)
Text("Caps hash: \(String(describing:capsVer))")
Divider()
if let capsSet = DataLayer.sharedInstance().getCapsforVer(capsVer, onAccountID:contact.obj.accountID) as? Set<String> {
let caps = Array(capsSet)
VStack(alignment: .leading) {
ForEach(caps, id: \.self) { cap in
Text(cap)
.font(.system(.footnote, design:.monospaced))
if cap != caps.last {
Divider()
}
}
}
}
}
}
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalXmppUserSoftWareVersionRefresh")).receive(on: RunLoop.main)) { notification in
if let xmppAccount = notification.object as? xmpp, let softwareInfo = notification.userInfo?["versionInfo"] as? MLContactSoftwareVersionInfo {
DDLogVerbose("Got software version info from account \(xmppAccount)...")
if softwareInfo.fromJid == contact.obj.contactJid && xmppAccount.accountID == contact.obj.accountID {
DispatchQueue.main.async {
DDLogVerbose("Successfully matched software version info update to current contact: \(contact.obj)")
self.contactVersionInfos[softwareInfo.resource ?? ""] = ObservableKVOWrapper<MLContactSoftwareVersionInfo>(softwareInfo)
}
}
}
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalNewPresenceNotice")).receive(on: RunLoop.main)) { notification in
if let xmppAccount = notification.object as? xmpp, let jid = notification.userInfo?["jid"] as? String, let resource = notification.userInfo?["resource"] as? String, let available = notification.userInfo?["available"] as? NSNumber {
DDLogVerbose("Got presence update from account \(xmppAccount)...")
if jid == contact.obj.contactJid && xmppAccount.accountID == contact.obj.accountID {
DispatchQueue.main.async {
DDLogVerbose("Successfully matched presence update to current contact: \(contact.obj)")
if available.boolValue {
if let softwareInfo = DataLayer.sharedInstance().getSoftwareVersionInfo(forContact:contact.obj.contactJid, resource:resource, andAccount:contact.obj.accountID) {
self.contactVersionInfos[resource] = ObservableKVOWrapper<MLContactSoftwareVersionInfo>(softwareInfo)
}
// query software version from contact
MLXMPPManager.sharedInstance().getEntitySoftWareVersion(for:contact.obj, andResource:resource)
} else {
self.contactVersionInfos[resource] = nil
}
}
}
}
}
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("kMonalLastInteractionUpdatedNotice")).receive(on: RunLoop.main)) { notification in
if let xmppAccount = notification.object as? xmpp, let jid = notification.userInfo?["jid"] as? String, let resource = notification.userInfo?["resource"] as? String, notification.userInfo?["lastInteraction"] as? NSDate != nil {
DDLogVerbose("Got lastInteraction update from account \(xmppAccount)...")
if jid == contact.obj.contactJid && xmppAccount.accountID == contact.obj.accountID {
DispatchQueue.main.async {
DDLogVerbose("Successfully matched lastInteraction update to current contact: \(contact.obj)")
self.contactVersionInfos[resource]?.obj.lastInteraction = DataLayer.sharedInstance().lastInteraction(ofJid:self.contact.obj.contactJid, andResource:resource, forAccountID:contact.obj.accountID)
}
}
}
}
.onAppear {
DDLogVerbose("View will appear...")
let newTimeout = DispatchTime.now() + 1.0;
DispatchQueue.main.asyncAfter(deadline: newTimeout) {
DDLogVerbose("Refreshing software version info...")
for ressourceName in DataLayer.sharedInstance().resources(for: contact.obj) {
// query software version from contact
MLXMPPManager.sharedInstance().getEntitySoftWareVersion(for:contact.obj, andResource:ressourceName)
}
}
}
.navigationBarTitle(Text("Devices of \(contact.contactDisplayName as String)"), displayMode: .inline)
}
}
func previewMock() -> [String:ObservableKVOWrapper<MLContactSoftwareVersionInfo>] {
var previewMock:[String:ObservableKVOWrapper<MLContactSoftwareVersionInfo>] = [:]
previewMock["m1"] = ObservableKVOWrapper<MLContactSoftwareVersionInfo>(MLContactSoftwareVersionInfo.init(jid: "test1@monal.im", andRessource: "m1", andAppName: "Monal", andAppVersion: "1.1.1", andPlatformOS: "ios", andLastInteraction: Date()))
previewMock["m2"] = ObservableKVOWrapper<MLContactSoftwareVersionInfo>(MLContactSoftwareVersionInfo.init(jid: "test1@monal.im", andRessource: "m2", andAppName: "Monal", andAppVersion: "1.1.2", andPlatformOS: "macOS", andLastInteraction: Date()))
previewMock["m3"] = ObservableKVOWrapper<MLContactSoftwareVersionInfo>(MLContactSoftwareVersionInfo.init(jid: "test1@monal.im", andRessource: "m3", andAppName: "Monal", andAppVersion: "1.1.2", andPlatformOS: "macOS", andLastInteraction: Date()))
return previewMock
}
struct ContactResources_Previews: PreviewProvider {
static var previews: some View {
ContactResources(contact:ObservableKVOWrapper<MLContact>(MLContact.makeDummyContact(0)), previewMock:previewMock())
}
}

View file

@ -0,0 +1,194 @@
//
// ContactsView.swift
// Monal
//
// Created by Matthew Fennell <matthew@fennell.dev> on 10/08/2024.
// Copyright © 2024 monal-im.org. All rights reserved.
//
import SwiftUI
struct ContactViewEntry: View {
private let contact: MLContact
@Binding private var selectedContactForContactDetails: ObservableKVOWrapper<MLContact>?
private let dismissWithContact: (MLContact) -> ()
@State private var shouldPresentRemoveContactAlert: Bool = false
private var removeContactButtonText: String {
if (!isDeletable) {
return "Cannot delete notes to self"
}
return contact.isMuc ? "Remove Conversation" : "Remove Contact"
}
private var removeContactConfirmationTitle: String {
contact.isMuc ? "Leave this converstion?" : "Remove \(contact.contactJid) from contacts?"
}
private var removeContactConfirmationDetail: String {
contact.isMuc ? "" : "They will no longer see when you are online. They may not be able to access your encryption keys."
}
private var isDeletable: Bool {
!contact.isSelfChat
}
init (contact: MLContact, selectedContactForContactDetails: Binding<ObservableKVOWrapper<MLContact>?>, dismissWithContact: @escaping (MLContact) -> ()) {
self.contact = contact
self._selectedContactForContactDetails = selectedContactForContactDetails
self.dismissWithContact = dismissWithContact
}
var body: some View {
// Apple's list dividers only extend as far left as the left-most text in the view.
// This means, by default, that the dividers on this screen would not extend all the way to the left of the view.
// This combination of HStack with spacing of 0, and empty text at the left of the view, is a workaround to override this behaviour.
// See https://stackoverflow.com/a/76698909
HStack(spacing: 0) {
Text("").frame(maxWidth: 0)
Button(action: { dismissWithContact(contact) }) {
HStack {
ContactEntry(contact: ObservableKVOWrapper<MLContact>(contact))
Spacer()
Button {
selectedContactForContactDetails = ObservableKVOWrapper<MLContact>(contact)
} label: {
Image(systemName: "info.circle")
.imageScale(.large)
}
.accessibilityLabel("Open contact details")
}
}
}
.swipeActions(allowsFullSwipe: false) {
// We do not use a Button with destructive role here as we would like to display the confirmation dialog first.
// A destructive role would dismiss the row immediately, without waiting for the confirmation.
Button(removeContactButtonText) {
shouldPresentRemoveContactAlert = true
}
.tint(isDeletable ? .red : .gray)
.disabled(!isDeletable)
}
.confirmationDialog(removeContactConfirmationTitle, isPresented: $shouldPresentRemoveContactAlert, titleVisibility: .visible) {
Button(role: .cancel) {} label: {
Text("No")
}
Button(role: .destructive) {
MLXMPPManager.sharedInstance().remove(contact)
} label: {
Text("Yes")
}
} message: {
Text(removeContactConfirmationDetail)
}
}
}
struct ContactsView: View {
@ObservedObject private var contacts: Contacts
private let delegate: SheetDismisserProtocol
private let dismissWithContact: (MLContact) -> ()
@State private var searchText: String = ""
@State private var selectedContactForContactDetails: ObservableKVOWrapper<MLContact>? = nil
init(contacts: Contacts, delegate: SheetDismisserProtocol, dismissWithContact: @escaping (MLContact) -> ()) {
self.contacts = contacts
self.delegate = delegate
self.dismissWithContact = dismissWithContact
}
private static func shouldDisplayContact(contact: MLContact) -> Bool {
#if IS_QUICKSY
return true
#endif
return contact.isSubscribedTo || contact.hasOutgoingContactRequest || contact.isSubscribedFrom
}
private var contactList: [MLContact] {
return contacts.contacts
.filter(ContactsView.shouldDisplayContact)
.sorted { ContactsView.sortingCriteria($0) < ContactsView.sortingCriteria($1) }
}
private var searchResults: [MLContact] {
if searchText.isEmpty { return contactList }
return contactList.filter { searchMatchesContact(contact: $0, search: searchText) }
}
private static func sortingCriteria(_ contact: MLContact) -> (String, String) {
return (contact.contactDisplayName.lowercased(), contact.contactJid.lowercased())
}
private func searchMatchesContact(contact: MLContact, search: String) -> Bool {
let jid = contact.contactJid.lowercased()
let name = contact.contactDisplayName.lowercased()
let search = search.lowercased()
return jid.contains(search) || name.contains(search)
}
var body: some View {
List {
ForEach(searchResults, id: \.self) { contact in
ContactViewEntry(contact: contact, selectedContactForContactDetails: $selectedContactForContactDetails, dismissWithContact: dismissWithContact)
}
}
.animation(.default, value: contactList)
.navigationTitle("Contacts")
.listStyle(.plain)
.searchable(text: $searchText, placement: .navigationBarDrawer(displayMode: .always))
.autocorrectionDisabled()
.textInputAutocapitalization(.never)
.keyboardType(.emailAddress)
.overlay {
if contactList.isEmpty {
ContentUnavailableShimView("You need friends for this ride", systemImage: "figure.wave", description: Text("Add new contacts with the + button above. Your friends will pop up here when they can talk"))
} else if searchResults.isEmpty {
ContentUnavailableShimView.search
}
}
.toolbar {
ToolbarItemGroup(placement: .topBarTrailing) {
NavigationLink(destination: CreateGroupMenu(delegate: SheetDismisserProtocol())) {
Image(systemName: "person.3.fill")
}
.accessibilityLabel("Create contact group")
NavigationLink(destination: AddContactMenu(delegate: SheetDismisserProtocol(), dismissWithNewContact: dismissWithContact)) {
Image(systemName: "person.fill.badge.plus")
.overlay { NumberlessBadge($contacts.requestCount) }
}
.accessibilityLabel(contacts.requestCount > 0 ? "Add contact (contact requests pending)" : "Add New Contact")
}
}
.sheet(item: $selectedContactForContactDetails) { selectedContact in
AnyView(AddTopLevelNavigation(withDelegate: delegate, to: ContactDetails(delegate:delegate, contact:selectedContact)))
}
}
}
class Contacts: ObservableObject {
@Published var contacts: Set<MLContact>
@Published var requestCount: Int
private var subscriptions: Set<AnyCancellable> = Set()
init() {
self.contacts = Set(DataLayer.sharedInstance().contactList())
self.requestCount = DataLayer.sharedInstance().allContactRequests().count
subscriptions = [
NotificationCenter.default.publisher(for: NSNotification.Name("kMonalContactRemoved"))
.receive(on: DispatchQueue.main)
.sink() { _ in self.refreshContacts() },
NotificationCenter.default.publisher(for: NSNotification.Name("kMonalContactRefresh"))
.receive(on: DispatchQueue.main)
.sink() { _ in self.refreshContacts() }
]
}
private func refreshContacts() {
self.contacts = Set(DataLayer.sharedInstance().contactList())
self.requestCount = DataLayer.sharedInstance().allContactRequests().count
}
}

View file

@ -0,0 +1,49 @@
//
// ContentUnavailableShimView.swift
// Monal
//
// Created by Matthew Fennell <matthew@fennell.dev> on 05/08/2024.
// Copyright © 2024 monal-im.org. All rights reserved.
//
import SwiftUI
struct ContentUnavailableShimView: View {
private var reason: String
private var systemImage: String
private var description: Text
init(_ reason: String, systemImage: String, description: Text) {
self.reason = reason
self.systemImage = systemImage
self.description = description
}
var body: some View {
if #available(iOS 17, *) {
ContentUnavailableView(reason, systemImage: systemImage, description: description)
} else {
VStack {
Image(systemName: systemImage)
.foregroundStyle(.secondary)
.font(.largeTitle)
.padding(.bottom, 4)
Text(reason)
.fontWeight(.bold)
.font(.title3)
description
.foregroundStyle(.secondary)
.font(.footnote)
.multilineTextAlignment(.center)
}
}
}
}
extension ContentUnavailableShimView {
static var search: ContentUnavailableShimView = ContentUnavailableShimView("No Results", systemImage: "magnifyingglass", description: Text("Check the spelling or try a new search."))
}
#Preview {
ContentUnavailableShimView("Cannot Display", systemImage: "iphone.homebutton.slash", description: Text("Cannot display for this reason."))
}

View file

@ -0,0 +1,141 @@
//
// AddContactMenu.swift
// Monal
//
// Created by Jan on 27.10.22.
// Copyright © 2022 monal-im.org. All rights reserved.
//
import OrderedCollections
struct CreateGroupMenu: View {
private var appDelegate: MonalAppDelegate
private var delegate: SheetDismisserProtocol
@State private var enabledAccounts: [xmpp]
@State private var selectedAccount: xmpp?
@State private var groupName: String = ""
@State private var showAlert = false
// note: dismissLabel is not accessed but defined at the .alert() section
@State private var alertPrompt = AlertPrompt(dismissLabel: Text("Close"))
@State private var selectedContacts: OrderedSet<ObservableKVOWrapper<MLContact>> = []
@State private var isEditingGroupName = false
@StateObject private var overlay = LoadingOverlayState()
init(delegate: SheetDismisserProtocol) {
self.appDelegate = UIApplication.shared.delegate as! MonalAppDelegate
self.delegate = delegate
let enabledAccounts = MLXMPPManager.sharedInstance().connectedXMPP as! [xmpp]
self.enabledAccounts = enabledAccounts
_selectedAccount = State(wrappedValue: enabledAccounts.first)
}
private func errorAlert(title: Text, message: Text = Text("")) {
alertPrompt.title = title
alertPrompt.message = message
showAlert = true
}
// When a Form is placed inside a Popover, and the horizontal size class is regular, the spacing chosen by SwiftUI is incorrect.
// In particular, the spacing between the top of the first element and the navigation bar is too small, meaning the two overlap.
// This only happens when the view is inside a popover, and the horizontal size class is regular.
// Therefore, it is inconvenient to apply some manual spacing, as this we would have to work out in which situations it should be applied.
// Placing a Text view inside the header causes SwiftUI to add consistent spacing in all situations.
var popoverFormSpacingWorkaround: some View {
Text("")
}
var body: some View {
Form {
if enabledAccounts.isEmpty {
Text("Please make sure at least one account has connected before trying to create new group.")
.foregroundColor(.secondary)
}
else
{
Section(header: popoverFormSpacingWorkaround) {
if enabledAccounts.count > 1 {
Picker(selection: $selectedAccount, label: Text("Use account")) {
ForEach(Array(self.enabledAccounts.enumerated()), id: \.element) { idx, account in
Text(account.connectionProperties.identity.jid).tag(account as xmpp?)
}
}
.pickerStyle(.menu)
}
TextField(NSLocalizedString("Group Name (optional)", comment: "placeholder when creating new group"), text: $groupName, onEditingChanged: { isEditingGroupName = $0 })
.autocorrectionDisabled()
.autocapitalization(.none)
.addClearButton(isEditing: isEditingGroupName, text:$groupName)
Button(action: {
guard let generatedJid = self.selectedAccount!.mucProcessor.generateMucJid() else {
errorAlert(title: Text("Error creating group!"), message: Text("Your server does not provide a MUC component."))
return
}
showLoadingOverlay(overlay, headline: NSLocalizedString("Creating Group", comment: ""))
guard let roomJid = self.selectedAccount!.mucProcessor.createGroup(generatedJid) else {
//room already existing in our local bookmarks --> just open it
//this should never happen since we randomly generated a jid above
hideLoadingOverlay(overlay)
let groupContact = MLContact.createContact(fromJid: generatedJid, andAccountID: self.selectedAccount!.accountID)
self.delegate.dismissWithoutAnimation()
if let activeChats = self.appDelegate.activeChats {
activeChats.presentChat(with:groupContact)
}
return
}
self.selectedAccount!.mucProcessor.addUIHandler({_data in let data = _data as! NSDictionary
let success : Bool = data["success"] as! Bool;
if success {
DataLayer.sharedInstance().setFullName(self.groupName, forContact:roomJid, andAccount:self.selectedAccount!.accountID)
self.selectedAccount!.mucProcessor.changeName(ofMuc: roomJid, to: self.groupName)
for user in self.selectedContacts {
self.selectedAccount!.mucProcessor.setAffiliation(kMucAffiliationMember, ofUser: user.contactJid, inMuc: roomJid)
self.selectedAccount!.mucProcessor.inviteUser(user.contactJid, inMuc: roomJid)
}
let groupContact = MLContact.createContact(fromJid: roomJid, andAccountID: self.selectedAccount!.accountID)
hideLoadingOverlay(overlay)
self.delegate.dismissWithoutAnimation()
if let activeChats = self.appDelegate.activeChats {
activeChats.presentChat(with:groupContact)
}
} else {
hideLoadingOverlay(overlay)
errorAlert(title: Text("Error creating group!"), message: Text(data["errorMessage"] as! String))
}
}, forMuc: roomJid)
}, label: {
Text("Create new group")
})
}
Section(header: Text("Selected Group Members")) {
NavigationLink(destination: LazyClosureView(ContactPicker(self.selectedAccount!, binding: $selectedContacts))) {
Text("Change Group Members")
}
ForEach(self.selectedContacts, id: \.obj.contactJid) { contact in
ContactEntry(contact: contact)
}
.onDelete(perform: { indexSet in
self.selectedContacts.remove(at: indexSet.first!)
})
}
}
}
.alert(isPresented: $showAlert) {
Alert(title: alertPrompt.title, message: alertPrompt.message, dismissButton:.default(Text("Close"), action: {
showAlert = false
}))
}
.addLoadingOverlay(overlay)
.navigationBarTitle(Text("Create new group"), displayMode: .inline)
}
}
struct CreateGroupMenu_Previews: PreviewProvider {
static var delegate = SheetDismisserProtocol()
static var previews: some View {
CreateGroupMenu(delegate: delegate)
}
}

329
Monal/Classes/DataLayer.h Normal file
View file

@ -0,0 +1,329 @@
//
// DataLayer.h
// SworIM
//
// Created by Anurodh Pokharel on 3/28/09.
// Copyright 2009 __MyCompanyName__. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "MLConstants.h"
#import "XMPPPresence.h"
#import "MLMessage.h"
#import "MLContact.h"
#import "MLContactSoftwareVersionInfo.h"
NS_ASSUME_NONNULL_BEGIN
@interface DataLayer : NSObject
extern NSString* const kAccountID;
extern NSString* const kAccountState;
extern NSString* const kDomain;
extern NSString* const kEnabled;
extern NSString* const kNeedsPasswordMigration;
extern NSString* const kPlainActivated;
extern NSString* const kServer;
extern NSString* const kPort;
extern NSString* const kResource;
extern NSString* const kDirectTLS;
extern NSString* const kRosterName;
extern NSString* const kUsername;
extern NSString* const kMessageTypeStatus;
extern NSString* const kMessageTypeMessageDraft;
extern NSString* const kMessageTypeText;
extern NSString* const kMessageTypeGeo;
extern NSString* const kMessageTypeUrl;
extern NSString* const kMessageTypeFiletransfer;
+(DataLayer*) sharedInstance;
-(NSString* _Nullable) exportDB;
-(void) createTransaction:(monal_void_block_t) block;
-(void) vacuum;
//Roster
-(NSString *) getRosterVersionForAccount:(NSNumber*) accountID;
-(void) setRosterVersion:(NSString *) version forAccount: (NSNumber*) accountID;
// Buddy Commands
-(BOOL) addContact:(NSString*) contact forAccount:(NSNumber*) accountID nickname:(NSString* _Nullable) nickName;
-(void) removeBuddy:(NSString*) buddy forAccount:(NSNumber*) accountID;
-(BOOL) clearBuddies:(NSNumber*) accountID;
-(NSDictionary* _Nullable) contactDictionaryForUsername:(NSString*) username forAccount: (NSNumber*) accountID;
/**
should be called when a new session needs to be established
*/
-(BOOL) resetContactsForAccount:(NSNumber*) accountID;
-(NSMutableArray<MLContact*>*) searchContactsWithString:(NSString*) search;
-(NSArray<MLContact*>*) contactList;
-(NSArray<MLContact*>*) contactListWithJid:(NSString*) jid;
-(NSArray<MLContact*>*) possibleGroupMembersForAccount:(NSNumber*) accountID;
-(NSArray<NSString*>*) resourcesForContact:(MLContact* _Nonnull)contact ;
-(MLContactSoftwareVersionInfo* _Nullable) getSoftwareVersionInfoForContact:(NSString*)contact resource:(NSString*)resource andAccount:(NSNumber*)account;
-(void) setSoftwareVersionInfoForContact:(NSString*)contact
resource:(NSString*)resource
andAccount:(NSNumber*)account
withSoftwareInfo:(MLContactSoftwareVersionInfo*) newSoftwareInfo;
#pragma mark Ver string and Capabilities
-(BOOL) checkCap:(NSString*) cap forUser:(NSString*) user onAccountID:(NSNumber*) accountID;
-(BOOL) checkCap:(NSString*) cap forUser:(NSString*) user andResource:(NSString*) resource onAccountID:(NSNumber*) accountID;
-(NSString*) getVerForUser:(NSString*) user andResource:(NSString*) resource onAccountID:(NSNumber*) accountID;
-(void) setVer:(NSString*) ver forUser:(NSString*) user andResource:(NSString*) resource onAccountID:(NSNumber*) accountID;
-(NSSet* _Nullable) getCapsforVer:(NSString*) ver onAccountID:(NSNumber*) accountID;
-(void) setCaps:(NSSet*) caps forVer:(NSString*) ver onAccountID:(NSNumber*) accountID;
#pragma mark presence functions
-(void) setResourceOnline:(XMPPPresence*) presenceObj forAccount:(NSNumber*) accountID;
-(void) setOnlineBuddy:(XMPPPresence*) presenceObj forAccount:(NSNumber*) accountID;
-(void) setOfflineBuddy:(XMPPPresence*) presenceObj forAccount:(NSNumber*) accountID;
-(void) setBuddyStatus:(XMPPPresence*) presenceObj forAccount:(NSNumber*) accountID;
-(NSString*) buddyStatus:(NSString*) buddy forAccount:(NSNumber*) accountID;
-(void) setBuddyState:(XMPPPresence*) presenceObj forAccount:(NSNumber*) accountID;
-(NSString*) buddyState:(NSString*) buddy forAccount:(NSNumber*) accountID;
-(BOOL) hasContactRequestForContact:(MLContact*) contact;
-(NSMutableArray*) allContactRequests;
-(void) addContactRequest:(MLContact *) requestor;
-(void) deleteContactRequest:(MLContact *) requestor;
#pragma mark Contact info
-(void) setFullName:(NSString*) fullName forContact:(NSString*) contact andAccount:(NSNumber*) accountID;
-(void) setAvatarHash:(NSString*) hash forContact:(NSString*) contact andAccount:(NSNumber*) accountID;
-(NSString*) getAvatarHashForContact:(NSString*) buddy andAccount:(NSNumber*) accountID;
-(BOOL) saveMessageDraft:(NSString*) buddy forAccount:(NSNumber*) accountID withComment:(NSString*) comment;
-(NSString*) loadMessageDraft:(NSString*) buddy forAccount:(NSNumber*) accountID;
#pragma mark - MUC
-(BOOL) initMuc:(NSString*) room forAccountID:(NSNumber*) accountID andMucNick:(NSString* _Nullable) mucNick;
-(void) cleanupParticipantsListFor:(NSString*) room onAccountID:(NSNumber*) accountID;
-(void) cleanupMembersListFor:(NSString*) room andType:(NSString*) type onAccountID:(NSNumber*) accountID;
-(void) addMember:(NSDictionary*) member toMuc:(NSString*) room forAccountID:(NSNumber*) accountID;
-(void) removeMember:(NSDictionary*) member fromMuc:(NSString*) room forAccountID:(NSNumber*) accountID;
-(void) addParticipant:(NSDictionary*) participant toMuc:(NSString*) room forAccountID:(NSNumber*) accountID;
-(void) removeParticipant:(NSDictionary*) participant fromMuc:(NSString*) room forAccountID:(NSNumber*) accountID;
-(NSDictionary* _Nullable) getParticipantForNick:(NSString*) nick inRoom:(NSString*) room forAccountID:(NSNumber*) accountID;
-(NSDictionary* _Nullable) getParticipantForOccupant:(NSString*) occupant inRoom:(NSString*) room forAccountID:(NSNumber*) accountID;
-(NSArray<NSDictionary<NSString*, id>*>*) getMembersAndParticipantsOfMuc:(NSString*) room forAccountID:(NSNumber*) accountID;
-(NSString* _Nullable) getOwnAffiliationInGroupOrChannel:(MLContact*) contact;
-(NSString* _Nullable) getOwnRoleInGroupOrChannel:(MLContact*) contact;
-(void) addMucFavorite:(NSString*) room forAccountID:(NSNumber*) accountID andMucNick:(NSString* _Nullable) mucNick;
-(NSString*) lastStanzaIdForMuc:(NSString* _Nonnull) room andAccount:(NSNumber* _Nonnull) accountID;
-(void) setLastStanzaId:(NSString*) lastStanzaId forMuc:(NSString* _Nonnull) room andAccount:(NSNumber* _Nonnull) accountID;
-(BOOL) isBuddyMuc:(NSString*) buddy forAccount:(NSNumber*) accountID;
-(NSString* _Nullable) ownNickNameforMuc:(NSString*) room forAccount:(NSNumber*) accountID;
-(BOOL) updateOwnNickName:(NSString*) nick forMuc:(NSString*) room forAccount:(NSNumber*) accountID;
-(BOOL) updateOwnOccupantID:(NSString* _Nullable) occupantID forMuc:(NSString*) room onAccountID:(NSNumber*) accountID;
-(NSString* _Nullable) getOwnOccupantIdForMuc:(NSString*) room onAccountID:(NSNumber*) accountID;
-(BOOL) updateMucSubject:(NSString*) subject forAccount:(NSNumber*) accountID andRoom:(NSString*) room;
-(NSString*) mucSubjectforAccount:(NSNumber*) accountID andRoom:(NSString*) room;
-(NSSet*) listMucsForAccount:(NSNumber*) accountID;
-(BOOL) deleteMuc:(NSString*) room forAccountID:(NSNumber*) accountID;
-(void) updateMucTypeTo:(NSString*) type forRoom:(NSString*) room andAccount:(NSNumber*) accountID;
-(NSString*) getMucTypeOfRoom:(NSString*) room andAccount:(NSNumber*) accountID;
/**
Calls with YES if contact has already been added to the database for this account
*/
-(BOOL) isContactInList:(NSString*) buddy forAccount:(NSNumber*) accountID;
#pragma mark - account commands
-(NSArray*) accountList;
-(NSNumber*) enabledAccountCnts;
-(NSArray*) enabledAccountList;
-(BOOL) isAccountEnabled:(NSNumber*) accountID;
-(BOOL) doesAccountExistUser:(NSString*) user andDomain:(NSString *) domain;
-(NSNumber* _Nullable) accountIDForUser:(NSString*) user andDomain:(NSString *) domain;
-(NSMutableDictionary* _Nullable) detailsForAccount:(NSNumber*) accountID;
-(BOOL) updateAccounWithDictionary:(NSDictionary *) dictionary;
-(NSNumber* _Nullable) addAccountWithDictionary:(NSDictionary *) dictionary;
-(BOOL) removeAccount:(NSNumber*) accountID;
/**
password migration
*/
-(BOOL) disableAccountForPasswordMigration:(NSNumber*) accountID;
-(NSArray*) accountListNeedingPasswordMigration;
-(BOOL) isPlainActivatedForAccount:(NSNumber*) accountID;
-(BOOL) deactivatePlainForAccount:(NSNumber*) accountID;
-(NSMutableDictionary* _Nullable) readStateForAccount:(NSNumber*) accountID;
-(void) persistState:(NSDictionary*) state forAccount:(NSNumber*) accountID;
#pragma mark - message Commands
/**
returns messages with the provided local id number
*/
-(NSArray<MLMessage*>*) messagesForHistoryIDs:(NSArray<NSNumber*>*) historyIDs;
-(MLMessage* _Nullable) messageForHistoryID:(NSNumber* _Nullable) historyID;
-(NSNumber*) getSmallestHistoryId;
-(NSNumber*) getBiggestHistoryId;
-(NSNumber* _Nullable) hasMessageForStanzaId:(NSString*) stanzaId orMessageID:(NSString*) messageId withInboundDir:(BOOL) inbound occupantId:(NSString* _Nullable) occupantId andJid:(NSString*) jid onAccount:(NSNumber*) accountID;
/*
adds a specified message to the database
*/
-(NSNumber*) addMessageToChatBuddy:(NSString*) buddyName withInboundDir:(BOOL) inbound forAccount:(NSNumber*) accountID withBody:(NSString*) message actuallyfrom:(NSString*) actualfrom occupantId:(NSString* _Nullable) occupantId participantJid:(NSString*_Nullable) participantJid sent:(BOOL) sent unread:(BOOL) unread messageId:(NSString*) messageid serverMessageId:(NSString*) stanzaid messageType:(NSString*) messageType andOverrideDate:(NSDate*) messageDate encrypted:(BOOL) encrypted displayMarkerWanted:(BOOL) displayMarkerWanted usingHistoryId:(NSNumber* _Nullable) historyId checkForDuplicates:(BOOL) checkForDuplicates;
/*
Marks a message as sent. When the server acked it
*/
-(void) setMessageId:(NSString*_Nonnull) messageid andJid:(NSString*) jid sent:(BOOL) sent;
/**
Marked when the client on the other end replies with a recived message
*/
-(void) setMessageId:( NSString* _Nonnull ) messageid andJid:(NSString*) jid received:(BOOL) received;
/**
if the server replies with an error for a message, store it
*/
-(void) setMessageId:(NSString* _Nonnull) messageid andJid:(NSString*) jid errorType:(NSString *_Nonnull) errorType errorReason:(NSString *_Nonnull)errorReason;
-(void) clearErrorOfMessageId:(NSString* _Nonnull) messageid;
/**
sets a preview info for a specified message
*/
-(void) setMessageId:(NSString*_Nonnull) messageid previewText:(NSString *) text andPreviewImage:(NSString *) image;
-(void) setMessageId:(NSString*) messageid stanzaId:(NSString *) stanzaId;
-(void) setMessageHistoryId:(NSNumber*) historyId filetransferMimeType:(NSString*) mimeType filetransferSize:(NSNumber*) size;
-(void) setMessageHistoryId:(NSNumber*) historyId messageType:(NSString*) messageType;
-(void) clearMessages:(NSNumber*) accountID;
-(void) clearMessagesWithBuddy:(NSString*) buddy onAccount:(NSNumber*) accountID;
-(NSNumber*) autoDeleteMessagesAfterInterval:(NSTimeInterval)interval;
-(void) retractMessageHistory:(NSNumber *) messageNo;
-(void) deleteMessageHistoryLocally:(NSNumber*) messageNo;
-(void) updateMessageHistory:(NSNumber*) messageNo withText:(NSString*) newText;
-(NSNumber* _Nullable) getLMCHistoryIDForMessageId:(NSString*) messageid from:(NSString*) from occupantId:(NSString* _Nullable) occupantId participantJid:(NSString* _Nullable) participantJid andAccount:(NSNumber*) accountID;
-(NSNumber* _Nullable) getRetractionHistoryIDForMessageId:(NSString*) messageid from:(NSString*) from participantJid:(NSString* _Nullable) participantJid occupantId:(NSString* _Nullable) occupantId andAccount:(NSNumber*) accountID;
-(NSNumber* _Nullable) getRetractionHistoryIDForModeratedStanzaId:(NSString*) stanzaId from:(NSString*) from andAccount:(NSNumber*) accountID;
-(NSDate* _Nullable) returnTimestampForQuote:(NSNumber*) historyID;
-(BOOL) checkLMCEligible:(NSNumber*) historyID encrypted:(BOOL) encrypted historyBaseID:(NSNumber* _Nullable) historyBaseID;
#pragma mark - message history
-(NSNumber*) lastMessageHistoryIdForContact:(NSString*) buddy forAccount:(NSNumber*) accountID;
-(NSMutableArray<MLMessage*>*) messagesForContact:(NSString*) buddy forAccount:(NSNumber*) accountID beforeMsgHistoryID:(NSNumber* _Nullable) msgHistoryID;
-(NSMutableArray<MLMessage*>*) messagesForContact:(NSString*) buddy forAccount:(NSNumber*) accountID;
-(MLMessage*) lastMessageForContact:(NSString*) contact forAccount:(NSNumber*) accountID;
-(NSString*) lastStanzaIdForAccount:(NSNumber*) accountID;
-(void) setLastStanzaId:(NSString*) lastStanzaId forAccount:(NSNumber*) accountID;
-(NSArray<MLMessage*>*) markMessagesAsReadForBuddy:(NSString*) buddy andAccount:(NSNumber*) accountID tillStanzaId:(NSString* _Nullable) stanzaId wasOutgoing:(BOOL) outgoing;
-(NSNumber*) addMessageHistoryTo:(NSString*) to forAccount:(NSNumber*) accountID withMessage:(NSString*) message actuallyFrom:(NSString*) actualfrom withId:(NSString*) messageId encrypted:(BOOL) encrypted messageType:(NSString*) messageType mimeType:(NSString* _Nullable) mimeType size:(NSNumber* _Nullable) size;
#pragma mark active contacts
-(NSMutableArray<MLContact*>*) activeContactsWithPinned:(BOOL) pinned;
-(NSArray<MLContact*>*) activeContactDict;
-(void) removeActiveBuddy:(NSString*) buddyname forAccount:(NSNumber*) accountID;
-(void) addActiveBuddies:(NSString*) buddyname forAccount:(NSNumber*) accountID;
-(BOOL) isActiveBuddy:(NSString*) buddyname forAccount:(NSNumber*) accountID;
-(BOOL) updateActiveBuddy:(NSString*) buddyname setTime:(NSString*)timestamp forAccount:(NSNumber*) accountID;
#pragma mark count unread
-(NSNumber*) countUserUnreadMessages:(NSString* _Nullable) buddy forAccount:(NSNumber* _Nullable) accountID;
-(NSNumber*) countUnreadMessages;
-(void) muteContact:(MLContact*) contact;
-(void) unMuteContact:(MLContact*) contact;
-(BOOL) isMutedJid:(NSString*) jid onAccount:(NSNumber*) accountID;
-(void) setMucAlertOnMentionOnly:(NSString*) jid onAccount:(NSNumber*) accountID;
-(void) setMucAlertOnAll:(NSString*) jid onAccount:(NSNumber*) accountID;
-(BOOL) isMucAlertOnMentionOnly:(NSString*) jid onAccount:(NSNumber*) accountID;
-(void) blockJid:(NSString*) jid withAccountID:(NSNumber*) accountID;
-(void) unBlockJid:(NSString*) jid withAccountID:(NSNumber*) accountID;
-(BOOL) isBlockedContact:(MLContact*) contact;
-(void) updateLocalBlocklistCache:(NSSet<NSString*>*) blockedJids forAccountID:(NSNumber*) accountID;
-(NSArray<NSString*>*) blockedJidsForAccount:(NSNumber*) accountID;
-(BOOL) isPinnedChat:(NSNumber*) accountID andBuddyJid:(NSString*) buddyJid;
-(void) pinChat:(NSNumber*) accountID andBuddyJid:(NSString*) buddyJid;
-(void) unPinChat:(NSNumber*) accountID andBuddyJid:(NSString*) buddyJid;
-(BOOL) shouldEncryptForJid:(NSString *) jid andAccountID:(NSNumber*) account;
-(void) encryptForJid:(NSString*) jid andAccountID:(NSNumber*) accountID;
-(void) disableEncryptForJid:(NSString*) jid andAccountID:(NSNumber*) accountID;
-(NSMutableArray*) allAttachmentsFromContact:(NSString*) contact forAccount:(NSNumber*) accountID;
-(NSDate* _Nullable) lastInteractionOfJid:(NSString* _Nonnull) jid forAccountID:(NSNumber* _Nonnull) accountID;
-(NSDate* _Nullable) lastInteractionOfJid:(NSString* _Nonnull) jid andResource:(NSString* _Nonnull) resource forAccountID:(NSNumber* _Nonnull) accountID;
-(void) setLastInteraction:(NSDate*) lastInteractionTime forJid:(NSString* _Nonnull) jid andResource:(NSString*) resource onAccountID:(NSNumber* _Nonnull) accountID;
-(NSDictionary *) getSubscriptionForContact:(NSString*) contact andAccount:(NSNumber*) accountID;
-(void) setSubscription:(NSString *)sub andAsk:(NSString*) ask forContact:(NSString*) contact andAccount:(NSNumber*) accountID;
-(void) setGroups:(NSSet*) groups forContact:(NSString*) contact inAccount:(NSNumber*) accountID;
#pragma mark History Message Search
/*
search message by keyword in message, buddy_name, messageType.
*/
-(NSArray* _Nullable) searchResultOfHistoryMessageWithKeyWords:(NSString* _Nonnull) keyword
accountID:(NSNumber* _Nonnull) accountID;
/*
search message by keyword in message, buddy_name, messageType.
*/
-(NSArray*) searchResultOfHistoryMessageWithKeyWords:(NSString*) keyword betweenContact:(MLContact* _Nonnull) contact;
-(NSArray<NSDictionary*>*) getAllCachedImages;
-(void) removeImageCacheTables;
-(NSArray*) getAllMessagesForFiletransferUrl:(NSString*) url;
-(void) upgradeImageMessagesToFiletransferMessages;
-(void) invalidateAllAccountStates;
-(NSString*) lastUsedPushServerForAccount:(NSNumber*) accountID;
-(void) updateUsedPushServer:(NSString*) pushServer forAccount:(NSNumber*) accountID;
-(void) deleteDelayedMessageStanzasForAccount:(NSNumber*) accountID;
-(void) addDelayedMessageStanza:(MLXMLNode*) stanza forArchiveJid:(NSString*) archiveJid andAccountID:(NSNumber*) accountID;
-(MLXMLNode* _Nullable) getNextDelayedMessageStanzaForArchiveJid:(NSString*) archiveJid andAccountID:(NSNumber*) accountID;
-(void) addShareSheetPayload:(NSDictionary*) payload;
-(NSArray*) getShareSheetPayload;
-(void) deleteShareSheetPayloadWithId:(NSNumber*) payloadId;
-(NSNumber*) addIdleTimerWithTimeout:(NSNumber*) timeout andHandler:(MLHandler*) handler onAccountID:(NSNumber*) accountID;
-(void) delIdleTimerWithId:(NSNumber* _Nullable) timerId;
-(void) cleanupIdleTimerOnAccountID:(NSNumber*) accountID;
-(void) decrementIdleTimersForAccount:(xmpp*) account;
@end
NS_ASSUME_NONNULL_END

2540
Monal/Classes/DataLayer.m Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,21 @@
//
// DataLayerMigrations.h
// monalxmpp
//
// Created by Friedrich Altheide on 15.01.22.
// Copyright © 2022 Monal.im. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "MLSQLite.h"
#import "DataLayer.h"
NS_ASSUME_NONNULL_BEGIN
@interface DataLayerMigrations : NSObject
+(BOOL) migrateDB:(MLSQLite*) db withDataLayer:(DataLayer*) dataLayer;
@end
NS_ASSUME_NONNULL_END

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,202 @@
//
// LogView.swift
// Monal
//
// Created by Zain Ashraf on 3/23/24.
// Copyright © 2024 monal-im.org. All rights reserved.
//
class DebugDefaultDB: ObservableObject {
@defaultsDB("udpLoggerEnabled")
var udpLoggerEnabled: Bool
@defaultsDB("udpLoggerPort")
var udpLoggerPort: String
@defaultsDB("udpLoggerHostname")
var udpLoggerHostname: String
@defaultsDB("udpLoggerKey")
var udpLoggerKey: String
@defaultsDB("hasCompletedOnboarding")
var hasCompletedOnboarding: Bool
}
struct LogFilesView: View {
@State private var sortedLogFileInfos: [DDLogFileInfo] = []
@State private var showShareSheet:Bool = false
@State private var fileURL: URL?
@State private var showingDBExportFailedAlert = false
func refreshSortedLogfiles() {
if let sortedLogFileInfos = HelperTools.fileLogger?.logFileManager.sortedLogFileInfos {
self.sortedLogFileInfos = sortedLogFileInfos
}
DispatchQueue.main.asyncAfter(deadline: .now() + 4.0) {
refreshSortedLogfiles()
}
}
var body: some View {
VStack(alignment: .leading) {
Text("This can be used to export logfiles.\n[Learn how to read them](https://github.com/monal-im/Monal/wiki/Introduction-to-Monal-Logging#view-the-log).")
List {
Section(header: Text("Logfiles")) {
ForEach(sortedLogFileInfos, id: \.self) { logFileInfo in
Button(logFileInfo.fileName) {
fileURL = URL(fileURLWithPath: logFileInfo.filePath)
}
}
}
Section(header: Text("Database Files")) {
Button("Main Database") {
if let dbFile = DataLayer.sharedInstance().exportDB() {
self.fileURL = URL(fileURLWithPath: dbFile)
} else {
showingDBExportFailedAlert = true
}
}
Button("IPC Database") {
if let dbFile = HelperTools.exportIPCDatabase() {
self.fileURL = URL(fileURLWithPath: dbFile)
} else {
showingDBExportFailedAlert = true
}
}
}
}
.listStyle(.grouped)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.interpolatedWindowBackground)
.alert(isPresented: $showingDBExportFailedAlert) {
Alert(title: Text("Database Export Failed"), message: Text("Failed to export the database, please check the logfile for errors and try again."), dismissButton: .default(Text("Close")))
}
.sheet(isPresented:$fileURL.optionalMappedToBool()) {
if let fileURL = fileURL {
ActivityViewController(activityItems: [fileURL])
}
}
.onAppear {
refreshSortedLogfiles()
}
}
}
struct UDPConfigView: View {
@ObservedObject var defaultDB = DebugDefaultDB()
var body: some View {
VStack(alignment: .leading) {
Text("The UDP logger allows you to livestream the log to the configured IP. Please use a secure key when streaming over the internet!\n[Learn how to receive the log stream](https://github.com/monal-im/Monal/wiki/Introduction-to-Monal-Logging#stream-the-log).")
Form {
Section(header: Text("UDP Logger Configuration")) {
Toggle(isOn: $defaultDB.udpLoggerEnabled) {
Text("Enable")
}
LabeledContent("Logserver IP:") {
TextField("Logserver IP", text: $defaultDB.udpLoggerHostname, prompt: Text("Required"))
}
LabeledContent("Logserver Port:") {
TextField("Logserver Port", text: $defaultDB.udpLoggerPort, prompt: Text("Required"))
}.keyboardType(.numberPad)
LabeledContent("AES Encryption Key:") {
TextField("AES Encryption Key", text: $defaultDB.udpLoggerKey, prompt: Text("Required"))
}
}
}
.padding(0)
.textFieldStyle(.roundedBorder)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.interpolatedWindowBackground)
}
}
struct CrashTestingView: View {
@ObservedObject var defaultDB = DebugDefaultDB()
var body: some View {
VStack(alignment:.leading, spacing: 25) {
Section(header: Text("Some debug settings.")) {
Toggle(isOn: $defaultDB.hasCompletedOnboarding) {
Text("Don't show onboarding")
}
}
Text("The following buttons allow you to forcefully crash the app using several different methods to test the crash handling.")
Group {
Button("Try to call unknown handler method") {
DispatchQueue.global(qos: .default).async(execute: {
HelperTools.flushLogs(withTimeout: 0.100)
let handler = MLHandler(delegate: self, handlerName: "IDontKnowThis", andBoundArguments: [:])
handler.call(withArguments: nil)
})
}
Button("Bad Access Crash") {
HelperTools.flushLogs(withTimeout: 0.100)
let delegate: AnyClass? = NSClassFromString("MonalAppDelegate")
print(delegate.unsafelyUnwrapped.audiovisualTypes())
}
Button("Assertion Crash") {
HelperTools.flushLogs(withTimeout: 0.100)
assert(false)
}
Button("Fatal Error Crash") {
HelperTools.flushLogs(withTimeout: 0.100)
fatalError("fatalError_example")
}
Button("Nil Crash") {
HelperTools.flushLogs(withTimeout: 0.100)
let crasher:Int? = nil
print(crasher!)
}
}.foregroundColor(.red)
Spacer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(.interpolatedWindowBackground)
}
}
struct DebugView: View {
@StateObject private var overlay = LoadingOverlayState()
var body: some View {
TabView {
LogFilesView()
.tabItem {
Image(systemName: "list.bullet")
Text("Logs")
}
UDPConfigView()
.tabItem {
Image(systemName: "gear")
Text("UDP Logger")
}
CrashTestingView()
.tabItem {
Image(systemName: "bolt.fill")
Text("Crash Testing")
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding()
.addLoadingOverlay(overlay)
.navigationBarItems(trailing:Button("Reconnect All") {
showLoadingOverlay(overlay, headline: "Reconnecting", description: "Will log out and reconnect all (connected) accounts.") {
MLXMPPManager.sharedInstance().reconnectAll()
return after(seconds:3.0)
}
})
}
}
#Preview {
NavigationStack {
DebugView()
}
}

View file

@ -0,0 +1,51 @@
//
// EditGroupSubject.swift
// Monal
//
// Created by Friedrich Altheide on 27.02.24.
// Copyright © 2024 monal-im.org. All rights reserved.
//
struct EditGroupSubject: View {
@StateObject var contact: ObservableKVOWrapper<MLContact>
private let account: xmpp?
@State private var subject: String
@Environment(\.presentationMode) var presentationMode
init(contact: ObservableKVOWrapper<MLContact>) {
MLAssert(contact.isMuc, "contact must be a muc")
_subject = State(wrappedValue: contact.obj.groupSubject)
_contact = StateObject(wrappedValue: contact)
self.account = contact.obj.account! as xmpp
}
var body: some View {
NavigationStack {
VStack {
Form {
Section(header: Text("Group Description (optional)")) {
TextEditor(text: $subject)
.multilineTextAlignment(.leading)
.lineLimit(10...50)
}
}
}
.navigationTitle(Text("Group/Channel description"))
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Abort") {
self.presentationMode.wrappedValue.dismiss()
}
}
ToolbarItem(placement: .confirmationAction) {
Button("Done") {
self.account!.mucProcessor.changeSubject(ofMuc: contact.contactJid, to: self.subject)
self.presentationMode.wrappedValue.dismiss()
}
}
}
}
}
}

View file

@ -0,0 +1,27 @@
//
// EncryptedPayload.swift
// MLCrypto
//
// Created by Anurodh Pokharel on 1/7/20.
// Copyright © 2020 Anurodh Pokharel. All rights reserved.
//
import UIKit
@objcMembers
public class EncryptedPayload: NSObject {
public var body: Data?
public var iv : Data?
public var key: Data?
public var tag: Data?
public var combined: Data?
@objc
public func updateValues(body:Data, iv: Data, key: Data, tag: Data)
{
self.body=body
self.iv=iv
self.key=key
self.tag=tag
}
}

View file

@ -0,0 +1,536 @@
//
// GeneralSettings.swift
// Monal
//
// Created by Vaidik Dubey on 22/03/24.
// Copyright © 2024 monal-im.org. All rights reserved.
//
import ViewExtractor
struct SettingsToggle<T>: View where T: View {
let value: Binding<Bool>
let contents: T
init(isOn value: Binding<Bool>, @ViewBuilder contents: @escaping () -> T) {
self.value = value
self.contents = contents()
}
var body:some View {
VStack(alignment: .leading, spacing: 0) {
Extract(contents) { views in
if views.count == 0 {
Text("")
} else {
Toggle(isOn: value) {
views[0]
.font(.body)
}
if views.count > 1 {
Group {
ForEach(views[1...]) { view in
view
.foregroundColor(Color(UIColor.secondaryLabel))
.font(.footnote)
}
}.fixedSize(horizontal: false, vertical: true).frame(maxWidth: .infinity, alignment: .leading)
}
}
}
}
}
}
func getNotificationPrivacyOption(_ option: NotificationPrivacySettingOption) -> String {
switch option{
case .DisplayNameAndMessage:
return NSLocalizedString("Display Name And Message", comment: "")
case .DisplayOnlyName:
return NSLocalizedString("Display Only Name", comment: "")
case .DisplayOnlyPlaceholder:
return NSLocalizedString("Display Only Placeholder", comment: "")
}
}
class GeneralSettingsDefaultsDB: ObservableObject {
@defaultsDB("NotificationPrivacySetting")
var notificationPrivacySetting: Int
@defaultsDB("OMEMODefaultOn")
var omemoDefaultOn:Bool
@defaultsDB("AutodeleteInterval")
var AutodeleteInterval: Int
@defaultsDB("SendLastUserInteraction")
var sendLastUserInteraction: Bool
@defaultsDB("SendLastChatState")
var sendLastChatState: Bool
@defaultsDB("SendReceivedMarkers")
var sendReceivedMarkers: Bool
@defaultsDB("SendDisplayedMarkers")
var sendDisplayedMarkers: Bool
@defaultsDB("ShowGeoLocation")
var showGeoLocation: Bool
@defaultsDB("ShowURLPreview")
var showURLPreview: Bool
@defaultsDB("useInlineSafari")
var useInlineSafari: Bool
@defaultsDB("webrtcAllowP2P")
var webrtcAllowP2P: Bool
@defaultsDB("webrtcUseFallbackTurn")
var webrtcUseFallbackTurn: Bool
@defaultsDB("allowVersionIQ")
var allowVersionIQ: Bool
@defaultsDB("allowNonRosterContacts")
var allowNonRosterContacts: Bool
@defaultsDB("allowCallsFromNonRosterContacts")
var allowCallsFromNonRosterContacts: Bool
@defaultsDB("AutodownloadFiletransfers")
var autodownloadFiletransfers : Bool
@defaultsDB("AutodownloadFiletransfersWifiMaxSize")
var autodownloadFiletransfersWifiMaxSize : UInt
@defaultsDB("AutodownloadFiletransfersMobileMaxSize")
var autodownloadFiletransfersMobileMaxSize : UInt
@defaultsDB("ImageUploadQuality")
var imageUploadQuality : Float
@defaultsDB("showKeyboardOnChatOpen")
var showKeyboardOnChatOpen: Bool
@defaultsDB("useDnssecForAllConnections")
var useDnssecForAllConnections: Bool
@defaultsDB("uploadImagesOriginal")
var uploadImagesOriginal: Bool
@defaultsDB("hardlinkFiletransfersIntoDocuments")
var hardlinkFiletransfersIntoDocuments: Bool
@defaultsDB("showAdvancedUI")
var showAdvancedUI: Bool
}
struct GeneralSettings: View {
@ObservedObject var generalSettingsDefaultsDB = GeneralSettingsDefaultsDB()
var body: some View {
Form {
Section(header:Text("General Settings")) {
NavigationLink(destination: LazyClosureView(UserInterfaceSettings())) {
HStack{
Image(systemName: "hand.tap.fill")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 20, height: 20)
Text("User Interface")
}
}
NavigationLink(destination: LazyClosureView(SecuritySettings())) {
HStack{
Image(systemName: "shield.checkerboard")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 20, height: 20)
Text("Security")
}
}
NavigationLink(destination: LazyClosureView(PrivacySettings())) {
HStack{
Image(systemName: "eye")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 20, height: 20)
Text("Privacy")
}
}
NavigationLink(destination: LazyClosureView(NotificationSettings())) {
HStack{
Image(systemName: "text.bubble")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 20, height: 20)
Text("Notifications")
}
}
NavigationLink(destination: LazyClosureView(AttachmentSettings())) {
HStack {
Image(systemName: "paperclip")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 20, height: 20)
Text("Attachments")
}
}
Button(action: {
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
}, label: {
HStack {
Image(systemName: "gear")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 20, height: 20)
#if targetEnvironment(macCatalyst)
Text("Open macOS settings")
#else
Text("Open iOS settings")
#endif
}.foregroundColor(Color(UIColor.label))
})
.buttonStyle(.borderless)
}
}
.navigationBarTitle(Text("General Settings"))
}
}
struct UserInterfaceSettings: View {
@ObservedObject var generalSettingsDefaultsDB = GeneralSettingsDefaultsDB()
var body: some View {
Form {
Section(header: Text("Previews")) {
SettingsToggle(isOn: $generalSettingsDefaultsDB.showGeoLocation) {
Text("Show inline geo location")
Text("Received geo locations are shared with Apple's Maps App.")
}
SettingsToggle(isOn: $generalSettingsDefaultsDB.showURLPreview) {
Text("Show URL previews")
Text("The operator of the webserver providing that URL may see your IP address.")
}
SettingsToggle(isOn: $generalSettingsDefaultsDB.useInlineSafari) {
Text("Open URLs inline in Safari")
Text("When disabled, URLs will opened in your default browser (that might not be Safari).")
}
}
Section(header: Text("Input")) {
SettingsToggle(isOn: $generalSettingsDefaultsDB.showKeyboardOnChatOpen) {
Text("Autofocus text input on chat open")
Text("Will focus the textfield on macOS or iOS with hardware keyboard attached, will open the software keyboard otherwise.")
}
}
Section(header: Text("Appearance")) {
VStack(alignment: .leading, spacing: 0) {
NavigationLink(destination: LazyClosureView(BackgroundSettings(contact:nil))) {
Text("Chat background image").font(.body)
}
Text("Configure the background image displayed in open chats.")
.foregroundColor(Color(UIColor.secondaryLabel))
.font(.footnote)
.fixedSize(horizontal: false, vertical: true)
}
SettingsToggle(isOn: $generalSettingsDefaultsDB.showAdvancedUI) {
Text("Show advanced options in UI")
Text("Show power-user options in settings and other parts of the user interface.")
}
}
}
.navigationBarTitle(Text("User Interface"), displayMode: .inline)
}
}
struct SecuritySettings: View {
@ObservedObject var generalSettingsDefaultsDB = GeneralSettingsDefaultsDB()
@State var autodeleteInterval: Int = 0
@State var autodeleteIntervalSelection: Int = 0
var autodeleteOptions = [
0: NSLocalizedString("Off", comment:"Message autdelete time"),
30: NSLocalizedString("30 seconds", comment:"Message autdelete time"),
60: NSLocalizedString("1 minute", comment:"Message autdelete time"),
300: NSLocalizedString("5 minutes", comment:"Message autdelete time"),
900: NSLocalizedString("15 minutes", comment:"Message autdelete time"),
1800: NSLocalizedString("30 minutes", comment:"Message autdelete time"),
3600: NSLocalizedString("1 hour", comment:"Message autdelete time"),
43200: NSLocalizedString("12 hours", comment:"Message autdelete time"),
86400: NSLocalizedString("1 day", comment:"Message autdelete time"),
259200: NSLocalizedString("3 days", comment:"Message autdelete time"),
604800: NSLocalizedString("1 week", comment:"Message autdelete time"),
2419200: NSLocalizedString("4 weeks", comment:"Message autdelete time"),
5184000: NSLocalizedString("2 month", comment:"Message autdelete time"), //based on 30 days per month
7776000: NSLocalizedString("3 month", comment:"Message autdelete time"), //based on 30 days per month
]
init() {
_autodeleteInterval = State(wrappedValue:generalSettingsDefaultsDB.AutodeleteInterval)
_autodeleteIntervalSelection = State(wrappedValue:generalSettingsDefaultsDB.AutodeleteInterval)
autodeleteOptions[-1] = NSLocalizedString("Custom", comment:"Message autdelete time")
//check if we have a custom value and change picker value accordingly
if autodeleteOptions[autodeleteInterval] == nil {
_autodeleteIntervalSelection = State(wrappedValue:-1)
}
}
var body: some View {
Form {
Section(header: Text("Encryption")) {
SettingsToggle(isOn: $generalSettingsDefaultsDB.omemoDefaultOn) {
Text("Enable encryption by default for new chats")
Text("Every new contact will have encryption enabled, but already known contacts will preserve their encryption settings.")
}
if generalSettingsDefaultsDB.showAdvancedUI {
SettingsToggle(isOn: $generalSettingsDefaultsDB.useDnssecForAllConnections) {
Text("Use DNSSEC validation for all connections")
Text(
"""
Use DNSSEC to validate all DNS query responses before connecting to the IP address designated \
in the DNS response.\n\
While being more secure, this can lead to connection problems in certain networks \
like hotel wifi, ugly mobile carriers etc.
"""
)
}
}
SettingsToggle(isOn: $generalSettingsDefaultsDB.webrtcAllowP2P) {
Text("Calls: Allow P2P sessions")
Text("Allow your device to establish a direct network connection to the remote party. This might leak your IP address to the caller/callee.")
}
}
Section(header: Text("On this device")) {
VStack(alignment: .leading, spacing: 0) {
Picker(selection: $autodeleteIntervalSelection, label: Text("Autodelete all messages older than")) {
ForEach(autodeleteOptions.keys.sorted(), id: \.self) { key in
Text(autodeleteOptions[key]!).tag(key)
}
}
//custom interval requested explicitly
if autodeleteIntervalSelection == -1 {
HStack {
Text("Custom Time: ")
Stepper(String(format:NSLocalizedString("%@ hours", comment:""), String(describing:(max(1, autodeleteInterval / 3600)).formatted())), value: Binding<Int>(
get: { max(1, autodeleteInterval / 3600) /*clamp to 1 ... .max*/ },
set: { autodeleteInterval = $0 * 3600 }
), in: 1 ... .max)
}
}
Text("Be warned: Message will only be deleted on incoming pushes or if you open the app! This is especially true for shorter time intervals!").foregroundColor(Color(UIColor.secondaryLabel)).font(.footnote)
Text("Also beware: You won't be able to load older history from your server, Monal will immediately delete it after fetching it!").foregroundColor(Color(UIColor.secondaryLabel)).font(.footnote)
}
}
}
.navigationBarTitle(Text("Security"), displayMode: .inline)
//save only when closing view to not delete messages while the user is selecting a (custom) value
.onDisappear {
if autodeleteIntervalSelection == -1 {
//make sure our custom value is stored clamped, too
autodeleteInterval = max(1, autodeleteInterval / 3600)
} else {
//copy over picker value if not set to custom
autodeleteInterval = autodeleteIntervalSelection
}
generalSettingsDefaultsDB.AutodeleteInterval = autodeleteInterval
}
}
}
struct PrivacySettings: View {
@ObservedObject var generalSettingsDefaultsDB = GeneralSettingsDefaultsDB()
var body: some View {
Form {
PrivacySettingsSubview(onboardingPart:-1)
}
.navigationBarTitle(Text("Privacy"), displayMode: .inline)
}
}
struct PrivacySettingsSubview: View {
@ObservedObject var generalSettingsDefaultsDB = GeneralSettingsDefaultsDB()
var onboardingPart: Int
var body: some View {
if onboardingPart == -1 || onboardingPart == 0 {
Section(header: Text("Activity indications")) {
SettingsToggle(isOn: $generalSettingsDefaultsDB.sendReceivedMarkers) {
Text("Send message receipts")
Text("Let your contacts know if you received a message.")
}
SettingsToggle(isOn: $generalSettingsDefaultsDB.sendDisplayedMarkers) {
Text("Send read receipts")
Text("Let your contacts know if you read a message.")
}
SettingsToggle(isOn: $generalSettingsDefaultsDB.sendLastChatState) {
Text("Send typing notifications")
Text("Let your contacts know if you are typing a message.")
}
SettingsToggle(isOn: $generalSettingsDefaultsDB.sendLastUserInteraction) {
Text("Send last interaction time")
Text("Let your contacts know when you last opened the app.")
}
}
}
if onboardingPart == -1 || onboardingPart == 1 {
Section(header: Text("Interactions")) {
SettingsToggle(isOn: $generalSettingsDefaultsDB.allowNonRosterContacts) {
Text("Accept incoming messages from strangers")
Text("Allow contacts not in your contact list to contact you.")
}
SettingsToggle(isOn: Binding<Bool>(
get: { generalSettingsDefaultsDB.allowCallsFromNonRosterContacts && generalSettingsDefaultsDB.allowNonRosterContacts },
set: { generalSettingsDefaultsDB.allowCallsFromNonRosterContacts = $0 }
)) {
Text("Accept incoming calls from strangers")
Text("Allow contacts not in your contact list to call you.")
}.disabled(!generalSettingsDefaultsDB.allowNonRosterContacts)
}
}
if onboardingPart == -1 || onboardingPart == 2 {
Section(header: Text("Misc")) {
SettingsToggle(isOn: $generalSettingsDefaultsDB.allowVersionIQ) {
Text("Publish version")
#if IS_QUICKSY
Text("Allow contacts in your contact list to query your Quicksy and iOS versions.")
#else
Text("Allow contacts in your contact list to query your Monal and iOS versions.")
#endif
}
//the quicksy.im server always has a proper TURN server, no need for this setting there
#if !IS_QUICKSY
SettingsToggle(isOn: $generalSettingsDefaultsDB.webrtcUseFallbackTurn) {
Text("Calls: Allow TURN fallback to Monal-Servers")
Text("This will make calls possible even if your XMPP server does not provide a TURN server, but leaks your IP to Monal's servers if your XMPP server does not provide a TURN server.")
}
#endif
}
}
}
}
struct NotificationSettings: View {
@ObservedObject var generalSettingsDefaultsDB = GeneralSettingsDefaultsDB()
@State private var pushPermissionEnabled = false
private var pushNotEnabled: Bool {
let xmppAccountInfo = MLXMPPManager.sharedInstance().connectedXMPP as! [xmpp]
var pushNotEnabled = false
for account in xmppAccountInfo {
pushNotEnabled = pushNotEnabled || !account.connectionProperties.pushEnabled
}
return pushNotEnabled
}
var body: some View {
Form {
Section(header: Text("Settings")) {
Picker(selection: $generalSettingsDefaultsDB.notificationPrivacySetting, label: Text("Privacy")) {
ForEach(NotificationPrivacySettingOption.allCases, id: \.self) { option in
Text(getNotificationPrivacyOption(option)).tag(option.rawValue)
}
}
.frame(height: 56, alignment: .trailing)
}
Section(header: Text("Debugging")) {
NavigationLink(destination: LazyClosureView(NotificationDebugging())) {
buildNotificationStateLabel(Text("Debug Notification Problems"), isWorking: !self.pushNotEnabled && self.pushPermissionEnabled)
}
}
}
.onAppear {
UNUserNotificationCenter.current().getNotificationSettings { (settings) -> Void in
self.pushPermissionEnabled = (settings.authorizationStatus == .authorized || settings.authorizationStatus == .provisional);
}
}
.navigationBarTitle(Text("Notifications"), displayMode: .inline)
}
}
struct AttachmentSettings: View {
@ObservedObject var generalSettingsDefaultsDB = GeneralSettingsDefaultsDB()
var body: some View {
Form {
Section(header: Text("General File Transfer Settings")) {
SettingsToggle(isOn: $generalSettingsDefaultsDB.autodownloadFiletransfers) {
Text("Auto-Download Media and Files")
}
SettingsToggle(isOn: $generalSettingsDefaultsDB.hardlinkFiletransfersIntoDocuments) {
Text("Make transfered Media and Files accessible in Files App")
}
}
Section(header: Text("Download Settings")) {
Text("Adjust the maximum file size for auto-downloads over WiFi")
.foregroundColor(.secondary)
.font(.footnote)
Slider(
value: $generalSettingsDefaultsDB.autodownloadFiletransfersWifiMaxSize.bytecount(mappedTo: 1024*1024),
in: 1.0...100.0,
step: 1.0,
minimumValueLabel: Text("1 MiB"),
maximumValueLabel: Text("100 MiB"),
label: {
Text("Load over wifi")
}
)
Text("Load over WiFi up to: \(String(describing:UInt(generalSettingsDefaultsDB.autodownloadFiletransfersWifiMaxSize/(1024*1024)))) MiB")
}
Section {
Text("Adjust the maximum file size for auto-downloads over cellular network")
.foregroundColor(.secondary)
.font(.footnote)
Slider(
value: $generalSettingsDefaultsDB.autodownloadFiletransfersMobileMaxSize.bytecount(mappedTo: 1024*1024),
in: 0.0...100.0,
step: 1.0,
minimumValueLabel: Text("1 MiB"),
maximumValueLabel: Text("100 MiB"),
label: {
Text("Load over Cellular")
}
)
Text("Load over cellular up to: \(String(describing:UInt(generalSettingsDefaultsDB.autodownloadFiletransfersMobileMaxSize/(1024*1024)))) MiB")
}
Section(header: Text("Upload Settings")) {
SettingsToggle(isOn: $generalSettingsDefaultsDB.uploadImagesOriginal) {
Text("Upload Original Images")
}
if !generalSettingsDefaultsDB.uploadImagesOriginal {
Text("Adjust the quality of images uploaded")
.foregroundColor(.secondary)
.font(.footnote)
Slider(
value: $generalSettingsDefaultsDB.imageUploadQuality,
in: 0.33...1.0,
step: 0.01,
minimumValueLabel: Text("33%"),
maximumValueLabel: Text("100%"),
label: {
Text("Upload Settings")
}
)
Text("Image Upload JPEG-Quality: \(String(format: "%.0f%%", generalSettingsDefaultsDB.imageUploadQuality*100))")
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
GeneralSettings()
}
}

View file

@ -0,0 +1,15 @@
//
// HelperTools+Quicksy_CountryCodes.h
// Monal
//
// Created by Thilo Molitor on 28.08.24.
// Copyright © 2024 Monal.im. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "HelperTools.h"
FOUNDATION_EXPORT NSArray* _Nonnull COUNTRY_CODES;
@implementation HelperTools (CountryCodes)
@end

View file

@ -0,0 +1,255 @@
// This file was automatically generated by scripts/itu_pdf_to_objc.py
// Please run this python script again to update this file
// Example ../scripts/itu_pdf_to_objc.py >Classes/HelperTools+Quicksy_CountryCodes.m
#import "Quicksy_Country.h"
#import "HelperTools.h"
NSArray* _Nonnull COUNTRY_CODES = @[]; //will be replaced by actual values in +load below
@implementation HelperTools (CountryCodes)
//see https://stackoverflow.com/a/13326633 and https://fek.io/blog/method-swizzling-in-obj-c-and-swift/
+(void) load
{
if(self == HelperTools.self)
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
COUNTRY_CODES = @[
[[Quicksy_Country alloc] initWithName:nil alpha2:@"AF" code:@"+93" pattern:@"^([0-9]{9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"AL" code:@"+355" pattern:@"^([0-9]{3,9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"DZ" code:@"+213" pattern:@"^([0-9]{8}|[0-9]{9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"AS" code:@"+1" pattern:@"^(684)([0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"AD" code:@"+376" pattern:@"^([0-9]{6}|[0-9]{8}|[0-9]{9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"AO" code:@"+244" pattern:@"^([0-9]{9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"AI" code:@"+1" pattern:@"^(264)([0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"AG" code:@"+1" pattern:@"^(268)([0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"AR" code:@"+54" pattern:@"^([0-9]{10})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"AM" code:@"+374" pattern:@"^([0-9]{8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"AW" code:@"+297" pattern:@"^([0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"AU" code:@"+61" pattern:@"^([0-9]{5,15})$"],
[[Quicksy_Country alloc] initWithName:NSLocalizedString(@"Australian External Territories", @"quicksy country") alpha2:nil code:@"+672" pattern:@"^([0-9]{6})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"AT" code:@"+43" pattern:@"^([0-9]{4,13})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"AZ" code:@"+994" pattern:@"^([0-9]{8,9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"BS" code:@"+1" pattern:@"^(242)([0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"BH" code:@"+973" pattern:@"^([0-9]{8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"BD" code:@"+880" pattern:@"^([0-9]{6,10})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"BB" code:@"+1" pattern:@"^(246)([0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"BY" code:@"+375" pattern:@"^([0-9]{9,10})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"BE" code:@"+32" pattern:@"^([0-9]{8,9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"BZ" code:@"+501" pattern:@"^([0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"BJ" code:@"+229" pattern:@"^([0-9]{8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"BM" code:@"+1" pattern:@"^(441)([0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"BT" code:@"+975" pattern:@"^([0-9]{7,8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"BO" code:@"+591" pattern:@"^([0-9]{8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"BQ" code:@"+599" pattern:@"^([0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"BA" code:@"+387" pattern:@"^([0-9]{8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"BW" code:@"+267" pattern:@"^([0-9]{7,8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"BR" code:@"+55" pattern:@"^([0-9]{10})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"VG" code:@"+1" pattern:@"^(284)([0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"BN" code:@"+673" pattern:@"^([0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"BG" code:@"+359" pattern:@"^([0-9]{7,9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"BF" code:@"+226" pattern:@"^([0-9]{8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"BI" code:@"+257" pattern:@"^([0-9]{8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"KH" code:@"+855" pattern:@"^([0-9]{8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"CM" code:@"+237" pattern:@"^([0-9]{8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"CA" code:@"+1" pattern:@"^([0-9]{10})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"CV" code:@"+238" pattern:@"^([0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"KY" code:@"+1" pattern:@"^(345)([0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"CF" code:@"+236" pattern:@"^([0-9]{8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"TD" code:@"+235" pattern:@"^([0-9]{8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"CL" code:@"+56" pattern:@"^([0-9]{8,9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"CN" code:@"+86" pattern:@"^([0-9]{5,12})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"CO" code:@"+57" pattern:@"^([0-9]{8}|[0-9]{10})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"KM" code:@"+269" pattern:@"^([0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"CG" code:@"+242" pattern:@"^([0-9]{9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"CK" code:@"+682" pattern:@"^([0-9]{5})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"CR" code:@"+506" pattern:@"^([0-9]{8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"CI" code:@"+225" pattern:@"^([0-9]{8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"HR" code:@"+385" pattern:@"^([0-9]{8,12})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"CU" code:@"+53" pattern:@"^([0-9]{6,8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"CW" code:@"+599" pattern:@"^([0-9]{7,8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"CY" code:@"+357" pattern:@"^([0-9]{8}|[0-9]{11})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"CZ" code:@"+420" pattern:@"^([0-9]{4,12})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"KP" code:@"+850" pattern:@"^([0-9]{6,17})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"CD" code:@"+243" pattern:@"^([0-9]{5,9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"DK" code:@"+45" pattern:@"^([0-9]{8})$"],
[[Quicksy_Country alloc] initWithName:NSLocalizedString(@"Diego Garcia", @"quicksy country") alpha2:nil code:@"+246" pattern:@"^([0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"DJ" code:@"+253" pattern:@"^([0-9]{6})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"DM" code:@"+1" pattern:@"^(767)([0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"DO" code:@"+1" pattern:@"^(809|829)([0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"EC" code:@"+593" pattern:@"^([0-9]{8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"EG" code:@"+20" pattern:@"^([0-9]{7,9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"SV" code:@"+503" pattern:@"^([0-9]{7}|[0-9]{8}|[0-9]{11})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"GQ" code:@"+240" pattern:@"^([0-9]{9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"ER" code:@"+291" pattern:@"^([0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"EE" code:@"+372" pattern:@"^([0-9]{7,10})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"ET" code:@"+251" pattern:@"^([0-9]{9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"FK" code:@"+500" pattern:@"^([0-9]{5})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"FO" code:@"+298" pattern:@"^([0-9]{6})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"FJ" code:@"+679" pattern:@"^([0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"FI" code:@"+358" pattern:@"^([0-9]{5,12})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"FR" code:@"+33" pattern:@"^([0-9]{9})$"],
[[Quicksy_Country alloc] initWithName:NSLocalizedString(@"French Departments and Territories in the Indian Ocean", @"quicksy country") alpha2:nil code:@"+262" pattern:@"^([0-9]{9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"GF" code:@"+594" pattern:@"^([0-9]{9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"PF" code:@"+689" pattern:@"^([0-9]{6})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"GA" code:@"+241" pattern:@"^([0-9]{6}|[0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"GM" code:@"+220" pattern:@"^([0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"GE" code:@"+995" pattern:@"^([0-9]{9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"DE" code:@"+49" pattern:@"^([0-9]{6,13})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"GH" code:@"+233" pattern:@"^([0-9]{5,9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"GI" code:@"+350" pattern:@"^([0-9]{8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"GR" code:@"+30" pattern:@"^([0-9]{10})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"GL" code:@"+299" pattern:@"^([0-9]{6})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"GD" code:@"+1" pattern:@"^(473)([0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"GP" code:@"+590" pattern:@"^([0-9]{9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"GU" code:@"+1" pattern:@"^(671)([0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"GT" code:@"+502" pattern:@"^([0-9]{8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"GN" code:@"+224" pattern:@"^([0-9]{8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"GW" code:@"+245" pattern:@"^([0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"GY" code:@"+592" pattern:@"^([0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"HT" code:@"+509" pattern:@"^([0-9]{8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"HN" code:@"+504" pattern:@"^([0-9]{8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"HK" code:@"+852" pattern:@"^([0-9]{4}|[0-9]{8,9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"HU" code:@"+36" pattern:@"^([0-9]{8,9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"IS" code:@"+354" pattern:@"^([0-9]{7}|[0-9]{9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"IN" code:@"+91" pattern:@"^([0-9]{7,10})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"ID" code:@"+62" pattern:@"^([0-9]{5,10})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"IR" code:@"+98" pattern:@"^([0-9]{6,10})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"IQ" code:@"+964" pattern:@"^([0-9]{8,10})$"],
[[Quicksy_Country alloc] initWithName:NSLocalizedString(@"Ireland", @"quicksy country") alpha2:nil code:@"+353" pattern:@"^([0-9]{7,11})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"IL" code:@"+972" pattern:@"^([0-9]{8,9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"IT" code:@"+39" pattern:@"^([0-9]{1,11})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"JM" code:@"+1" pattern:@"^(876)([0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"JP" code:@"+81" pattern:@"^([0-9]{5,13})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"JO" code:@"+962" pattern:@"^([0-9]{5,9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"KZ" code:@"+7" pattern:@"^([0-9]{10})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"KE" code:@"+254" pattern:@"^([0-9]{6,10})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"KI" code:@"+686" pattern:@"^([0-9]{5})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"KR" code:@"+82" pattern:@"^([0-9]{8,11})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"KW" code:@"+965" pattern:@"^([0-9]{7}|[0-9]{8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"KG" code:@"+996" pattern:@"^([0-9]{9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"LA" code:@"+856" pattern:@"^([0-9]{8,10})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"LV" code:@"+371" pattern:@"^([0-9]{7}|[0-9]{8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"LB" code:@"+961" pattern:@"^([0-9]{7,8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"LS" code:@"+266" pattern:@"^([0-9]{8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"LR" code:@"+231" pattern:@"^([0-9]{7,8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"LY" code:@"+218" pattern:@"^([0-9]{8,9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"LI" code:@"+423" pattern:@"^([0-9]{7,9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"LT" code:@"+370" pattern:@"^([0-9]{8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"LU" code:@"+352" pattern:@"^([0-9]{4,11})$"],
[[Quicksy_Country alloc] initWithName:NSLocalizedString(@"Macao, China", @"quicksy country") alpha2:nil code:@"+853" pattern:@"^([0-9]{7,8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"MG" code:@"+261" pattern:@"^([0-9]{9,10})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"MW" code:@"+265" pattern:@"^([0-9]{7}|[0-9]{8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"MY" code:@"+60" pattern:@"^([0-9]{7,9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"MV" code:@"+960" pattern:@"^([0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"ML" code:@"+223" pattern:@"^([0-9]{8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"MT" code:@"+356" pattern:@"^([0-9]{8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"MH" code:@"+692" pattern:@"^([0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"MQ" code:@"+596" pattern:@"^([0-9]{9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"MR" code:@"+222" pattern:@"^([0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"MU" code:@"+230" pattern:@"^([0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"MX" code:@"+52" pattern:@"^([0-9]{10})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"FM" code:@"+691" pattern:@"^([0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"MD" code:@"+373" pattern:@"^([0-9]{8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"MC" code:@"+377" pattern:@"^([0-9]{5,9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"MN" code:@"+976" pattern:@"^([0-9]{7,8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"ME" code:@"+382" pattern:@"^([0-9]{4,12})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"MS" code:@"+1" pattern:@"^(664)([0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"MA" code:@"+212" pattern:@"^([0-9]{9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"MZ" code:@"+258" pattern:@"^([0-9]{8,9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"MM" code:@"+95" pattern:@"^([0-9]{7,9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"NA" code:@"+264" pattern:@"^([0-9]{6,10})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"NR" code:@"+674" pattern:@"^([0-9]{4}|[0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"NP" code:@"+977" pattern:@"^([0-9]{8,9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"NL" code:@"+31" pattern:@"^([0-9]{9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"NC" code:@"+687" pattern:@"^([0-9]{6})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"NZ" code:@"+64" pattern:@"^([0-9]{3,10})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"NI" code:@"+505" pattern:@"^([0-9]{8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"NE" code:@"+227" pattern:@"^([0-9]{8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"NG" code:@"+234" pattern:@"^([0-9]{7,10})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"NU" code:@"+683" pattern:@"^([0-9]{4})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"MP" code:@"+1" pattern:@"^(670)([0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"NO" code:@"+47" pattern:@"^([0-9]{5}|[0-9]{8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"OM" code:@"+968" pattern:@"^([0-9]{7,8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"PK" code:@"+92" pattern:@"^([0-9]{8,11})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"PW" code:@"+680" pattern:@"^([0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"PA" code:@"+507" pattern:@"^([0-9]{7}|[0-9]{8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"PG" code:@"+675" pattern:@"^([0-9]{4,11})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"PY" code:@"+595" pattern:@"^([0-9]{5,9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"PE" code:@"+51" pattern:@"^([0-9]{8,11})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"PH" code:@"+63" pattern:@"^([0-9]{8,10})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"PL" code:@"+48" pattern:@"^([0-9]{6,9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"PT" code:@"+351" pattern:@"^([0-9]{9}|[0-9]{11})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"PR" code:@"+1" pattern:@"^(787|939)([0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"QA" code:@"+974" pattern:@"^([0-9]{3,8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"RO" code:@"+40" pattern:@"^([0-9]{9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"RU" code:@"+7" pattern:@"^([0-9]{10})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"RW" code:@"+250" pattern:@"^([0-9]{9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"SH" code:@"+247" pattern:@"^([0-9]{4})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"SH" code:@"+290" pattern:@"^([0-9]{4})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"KN" code:@"+1" pattern:@"^(869)([0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"LC" code:@"+1" pattern:@"^(758)([0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"PM" code:@"+508" pattern:@"^([0-9]{6})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"VC" code:@"+1" pattern:@"^(784)([0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"WS" code:@"+685" pattern:@"^([0-9]{3,7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"SM" code:@"+378" pattern:@"^([0-9]{6,10})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"ST" code:@"+239" pattern:@"^([0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"SA" code:@"+966" pattern:@"^([0-9]{8,9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"SN" code:@"+221" pattern:@"^([0-9]{9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"RS" code:@"+381" pattern:@"^([0-9]{4,12})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"SC" code:@"+248" pattern:@"^([0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"SL" code:@"+232" pattern:@"^([0-9]{8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"SG" code:@"+65" pattern:@"^([0-9]{8,12})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"SX" code:@"+1" pattern:@"^(721)([0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"SK" code:@"+421" pattern:@"^([0-9]{4,9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"SI" code:@"+386" pattern:@"^([0-9]{8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"SB" code:@"+677" pattern:@"^([0-9]{5})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"SO" code:@"+252" pattern:@"^([0-9]{5,8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"ZA" code:@"+27" pattern:@"^([0-9]{9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"ES" code:@"+34" pattern:@"^([0-9]{9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"LK" code:@"+94" pattern:@"^([0-9]{9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"SD" code:@"+249" pattern:@"^([0-9]{9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"SR" code:@"+597" pattern:@"^([0-9]{6,7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"SZ" code:@"+268" pattern:@"^([0-9]{7,8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"SE" code:@"+46" pattern:@"^([0-9]{7,13})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"CH" code:@"+41" pattern:@"^([0-9]{4,12})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"SY" code:@"+963" pattern:@"^([0-9]{8,10})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"TW" code:@"+886" pattern:@"^([0-9]{8,9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"TJ" code:@"+992" pattern:@"^([0-9]{9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"TZ" code:@"+255" pattern:@"^([0-9]{9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"TH" code:@"+66" pattern:@"^([0-9]{8}|[0-9]{9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"MK" code:@"+389" pattern:@"^([0-9]{8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"TL" code:@"+670" pattern:@"^([0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"TG" code:@"+228" pattern:@"^([0-9]{8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"TK" code:@"+690" pattern:@"^([0-9]{4})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"TO" code:@"+676" pattern:@"^([0-9]{5}|[0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"TT" code:@"+1" pattern:@"^(868)([0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"TN" code:@"+216" pattern:@"^([0-9]{8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"TR" code:@"+90" pattern:@"^([0-9]{10})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"TM" code:@"+993" pattern:@"^([0-9]{8})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"TC" code:@"+1" pattern:@"^(649)([0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"TV" code:@"+688" pattern:@"^([0-9]{5}|[0-9]{6})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"UG" code:@"+256" pattern:@"^([0-9]{9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"UA" code:@"+380" pattern:@"^([0-9]{9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"AE" code:@"+971" pattern:@"^([0-9]{8,9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"GB" code:@"+44" pattern:@"^([0-9]{7,10})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"US" code:@"+1" pattern:@"^([0-9]{10})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"VI" code:@"+1" pattern:@"^(340)([0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"UY" code:@"+598" pattern:@"^([0-9]{4,11})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"UZ" code:@"+998" pattern:@"^([0-9]{9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"VU" code:@"+678" pattern:@"^([0-9]{5}|[0-9]{7})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"VA" code:@"+39" pattern:@"^([0-9]{1,11})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"VE" code:@"+58" pattern:@"^([0-9]{10})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"VN" code:@"+84" pattern:@"^([0-9]{7,10})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"WF" code:@"+681" pattern:@"^([0-9]{6})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"YE" code:@"+967" pattern:@"^([0-9]{6,9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"ZM" code:@"+260" pattern:@"^([0-9]{9})$"],
[[Quicksy_Country alloc] initWithName:nil alpha2:@"ZW" code:@"+263" pattern:@"^([0-9]{5,10})$"],
];
});
}
}
@end

224
Monal/Classes/HelperTools.h Normal file
View file

@ -0,0 +1,224 @@
//
// HelperTools.h
// Monal
//
// Created by Friedrich Altheide on 08.07.20.
// Copyright © 2020 Monal.im. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "MLConstants.h"
#import "MLDelayableTimer.h"
#include "metamacros.h"
#define createDelayableTimer(timeout, handler, ...) createDelayableQueuedTimer(timeout, nil, handler, __VA_ARGS__)
#define createDelayableQueuedTimer(timeout, queue, handler, ...) metamacro_if_eq(0, metamacro_argcount(__VA_ARGS__))([HelperTools startDelayableQueuedTimer:timeout withHandler:handler andCancelHandler:nil andFile:(char*)__FILE__ andLine:__LINE__ andFunc:(char*)__func__ onQueue:queue])(_createDelayableQueuedTimer(timeout, queue, handler, __VA_ARGS__))
#define _createDelayableQueuedTimer(timeout, queue, handler, cancelHandler, ...) [HelperTools startDelayableQueuedTimer:timeout withHandler:handler andCancelHandler:cancelHandler andFile:(char*)__FILE__ andLine:__LINE__ andFunc:(char*)__func__ onQueue:queue]
#define createTimer(timeout, handler, ...) createQueuedTimer(timeout, nil, handler, __VA_ARGS__)
#define createQueuedTimer(timeout, queue, handler, ...) metamacro_if_eq(0, metamacro_argcount(__VA_ARGS__))([HelperTools startQueuedTimer:timeout withHandler:handler andCancelHandler:nil andFile:(char*)__FILE__ andLine:__LINE__ andFunc:(char*)__func__ onQueue:queue])(_createQueuedTimer(timeout, queue, handler, __VA_ARGS__))
#define _createQueuedTimer(timeout, queue, handler, cancelHandler, ...) [HelperTools startQueuedTimer:timeout withHandler:handler andCancelHandler:cancelHandler andFile:(char*)__FILE__ andLine:__LINE__ andFunc:(char*)__func__ onQueue:queue]
#define MLAssert(check, text, ...) do { if(!(check)) { metamacro_if_eq(0, metamacro_argcount(__VA_ARGS__))([HelperTools MLAssertWithText:text andUserData:nil andFile:(char*)__FILE__ andLine:__LINE__ andFunc:(char*)__func__];)([HelperTools MLAssertWithText:text andUserData:metamacro_head(__VA_ARGS__) andFile:(char*)__FILE__ andLine:__LINE__ andFunc:(char*)__func__];) while(YES); } } while(0)
#define unreachable(...) do { metamacro_if_eq(0, metamacro_argcount(__VA_ARGS__))(MLAssert(NO, @"unreachable", __VA_ARGS__);)(MLAssert(NO, __VA_ARGS__);); } while(0)
#define showErrorOnAlpha(account, description, ...) do { [HelperTools showErrorOnAlpha:[NSString stringWithFormat:description, ##__VA_ARGS__] withNode:nil andAccount:account andFile:(char*)__FILE__ andLine:__LINE__ andFunc:(char*)__func__]; } while(0)
#define showXMLErrorOnAlpha(account, node, description, ...) do { [HelperTools showErrorOnAlpha:[NSString stringWithFormat:description, ##__VA_ARGS__] withNode:node andAccount:account andFile:(char*)__FILE__ andLine:__LINE__ andFunc:(char*)__func__]; } while(0)
NS_ASSUME_NONNULL_BEGIN
@class AnyPromise;
@class MLXMLNode;
@class xmpp;
@class XMPPStanza;
@class UNNotificationRequest;
@class DDLogMessage;
@class DDFileLogger;
@class UIView;
@class UITapGestureRecognizer;
@class AVURLAsset;
typedef NS_ENUM(NSUInteger, MLVersionType) {
MLVersionTypeIQ,
MLVersionTypeLog,
};
typedef NS_ENUM(NSUInteger, MLDefinedIdentifier) {
MLDefinedIdentifier_kAppGroup,
MLDefinedIdentifier_kMonalOpenURL,
MLDefinedIdentifier_kBackgroundProcessingTask,
MLDefinedIdentifier_kBackgroundRefreshingTask,
MLDefinedIdentifier_kMonalKeychainName,
MLDefinedIdentifier_kMucTypeGroup,
MLDefinedIdentifier_kMucTypeChannel,
MLDefinedIdentifier_kMucRoleModerator,
MLDefinedIdentifier_kMucRoleNone,
MLDefinedIdentifier_kMucRoleParticipant,
MLDefinedIdentifier_kMucRoleVisitor,
MLDefinedIdentifier_kMucAffiliationOwner,
MLDefinedIdentifier_kMucAffiliationAdmin,
MLDefinedIdentifier_kMucAffiliationMember,
MLDefinedIdentifier_kMucAffiliationOutcast,
MLDefinedIdentifier_kMucAffiliationNone,
MLDefinedIdentifier_kMucActionShowProfile,
MLDefinedIdentifier_kMucActionReinvite,
MLDefinedIdentifier_SHORT_PING,
MLDefinedIdentifier_LONG_PING,
MLDefinedIdentifier_MUC_PING,
MLDefinedIdentifier_BGFETCH_DEFAULT_INTERVAL,
};
typedef NS_ENUM(NSUInteger, MLRunLoopIdentifier) {
MLRunLoopIdentifierNetwork,
MLRunLoopIdentifierTimer,
};
void logException(NSException* exception);
void swizzle(Class c, SEL orig, SEL new);
//weak container holding an object as weak pointer (needed to not create retain circles in NSCache
@interface WeakContainer : NSObject
@property (atomic, weak) id obj;
-(id) initWithObj:(id) obj;
@end
@interface HelperTools : NSObject
@property (class, nonatomic, strong, nullable) DDFileLogger* fileLogger;
+(NSData* _Nullable) convertLogmessageToJsonData:(DDLogMessage*) logMessage counter:(uint64_t*) counter andError:(NSError** _Nullable) error;
+(void) initSystem;
+(void) installExceptionHandler;
+(int) pendingCrashreportCount;
+(void) flushLogsWithTimeout:(double) timeout;
+(void) __attribute__((noreturn)) MLAssertWithText:(NSString*) text andUserData:(id _Nullable) additionalData andFile:(const char* const) file andLine:(int) line andFunc:(const char* const) func;
+(void) __attribute__((noreturn)) handleRustPanicWithText:(NSString*) text andBacktrace:(NSString*) backtrace;
+(void) __attribute__((noreturn)) throwExceptionWithName:(NSString*) name reason:(NSString*) reason userInfo:(NSDictionary* _Nullable) userInfo;
+(void) postError:(NSString*) description withNode:(XMPPStanza* _Nullable) node andAccount:(xmpp*) account andIsSevere:(BOOL) isSevere andDisableAccount:(BOOL) disableAccount;
+(void) postError:(NSString*) description withNode:(XMPPStanza* _Nullable) node andAccount:(xmpp*) account andIsSevere:(BOOL) isSevere;
+(NSString*) extractXMPPError:(XMPPStanza*) stanza withDescription:(NSString* _Nullable) description;
+(void) showErrorOnAlpha:(NSString*) description withNode:(XMPPStanza* _Nullable) node andAccount:(xmpp* _Nullable) account andFile:(char*) file andLine:(int) line andFunc:(char*) func;
+(NSDictionary<NSString*, NSString*>*) getInvalidPushServers;
+(NSString*) getSelectedPushServerBasedOnLocale;
+(NSDictionary<NSString*, NSString*>*) getAvailablePushServers;
+(void) configureDefaultAudioSession;
+(NSArray<NSString*>*) getFailoverStunServers;
+(NSURL*) getFailoverTurnApiServer;
+(NSArray<MLXMLNode*>* _Nullable) sdp2xml:(NSString*) sdp withInitiator:(BOOL) initiator;
+(NSString* _Nullable) xml2sdp:(MLXMLNode*) xml withInitiator:(BOOL) initiator;
+(MLXMLNode* _Nullable) candidate2xml:(NSString*) candidate withMid:(NSString*) mid pwd:(NSString* _Nullable) pwd ufrag:(NSString* _Nullable) ufrag andInitiator:(BOOL) initiator;
+(NSString* _Nullable) xml2candidate:(MLXMLNode*) xml withInitiator:(BOOL) initiator;
+(AnyPromise*) renderUIImageFromSVGURL:(NSURL* _Nullable) url;
+(AnyPromise*) renderUIImageFromSVGData:(NSData* _Nullable) data;
+(void) busyWaitForOperationQueue:(NSOperationQueue*) queue;
+(id) getObjcDefinedValue:(MLDefinedIdentifier) identifier;
+(NSRunLoop*) getExtraRunloopWithIdentifier:(MLRunLoopIdentifier) identifier;
+(NSError* _Nullable) hardLinkOrCopyFile:(NSString*) from to:(NSString*) to;
+(NSString*) getQueueThreadLabelFor:(DDLogMessage*) logMessage;
+(BOOL) shouldProvideVoip;
+(BOOL) isSandboxAPNS;
+(int) compareIOcted:(NSData*) data1 with:(NSData*) data2;
+(NSURL*) getContainerURLForPathComponents:(NSArray*) components;
+(NSURL*) getSharedDocumentsURLForPathComponents:(NSArray*) components;
+(NSData*) serializeObject:(id) obj;
+(id) unserializeData:(NSData*) data;
+(NSError* _Nullable) postUserNotificationRequest:(UNNotificationRequest*) request;
+(void) createAVURLAssetFromFile:(NSString*) file havingMimeType:(NSString*) mimeType andFileExtension:(NSString* _Nullable) fileExtension withCompletionHandler:(void(^)(AVURLAsset* _Nullable)) completion;
+(AnyPromise*) generateVideoThumbnailFromFile:(NSString*) file havingMimeType:(NSString*) mimeType andFileExtension:(NSString* _Nullable) fileExtension;
+(void) addUploadItemPreviewForItem:(NSURL* _Nullable) url provider:(NSItemProvider* _Nullable) provider andPayload:(NSMutableDictionary*) payload withCompletionHandler:(void(^)(NSMutableDictionary* _Nullable)) completion;
+(void) handleUploadItemProvider:(NSItemProvider*) provider withCompletionHandler:(void (^)(NSMutableDictionary* _Nullable)) completion;
+(UIImage* _Nullable) rotateImage:(UIImage* _Nullable) image byRadians:(CGFloat) rotation;
+(UIImage* _Nullable) mirrorImageOnXAxis:(UIImage* _Nullable) image;
+(UIImage* _Nullable) mirrorImageOnYAxis:(UIImage* _Nullable) image;
+(UIImage*) imageWithNotificationBadgeForImage:(UIImage*) image;
+(UIView*) buttonWithNotificationBadgeForImage:(UIImage*) image hasNotification:(bool) hasNotification withTapHandler: (UITapGestureRecognizer*) handler;
+(NSData*) resizeAvatarImage:(UIImage* _Nullable) image withCircularMask:(BOOL) circularMask toMaxBase64Size:(unsigned long) length;
+(double) report_memory;
+(UIColor*) generateColorFromJid:(NSString*) jid;
+(NSString*) bytesToHuman:(int64_t) bytes;
+(NSString*) stringFromToken:(NSData*) tokenIn;
+(NSString* _Nullable) exportIPCDatabase;
+(void) configureFileProtection:(NSString*) protectionLevel forFile:(NSString*) file;
+(void) configureFileProtectionFor:(NSString*) file;
+(BOOL) isContactBlacklistedForEncryption:(MLContact*) contact NS_SWIFT_NAME(isContactBlacklistedForEncryption(_:));
+(void) removeAllShareInteractionsForAccountID:(NSNumber*) accountID;
+(NSDictionary<NSString*, NSString*>*) splitJid:(NSString*) jid;
+(void) scheduleBackgroundTask:(BOOL) force;
+(void) clearSyncErrorsOnAppForeground;
+(void) removePendingSyncErrorNotifications;
+(void) updateSyncErrorsWithDeleteOnly:(BOOL) removeOnly andWaitForCompletion:(BOOL) waitForCompletion;
+(BOOL) isInBackground;
+(BOOL) isNotInFocus;
+(void) dispatchAsync:(BOOL) async reentrantOnQueue:(dispatch_queue_t _Nullable) queue withBlock:(monal_void_block_t) block;
+(NSUserDefaults*) defaultsDB;
+(BOOL) isAppExtension;
+(NSString*) generateStringOfFeatureSet:(NSSet*) features;
+(NSSet*) getOwnFeatureSet;
+(NSString*) getEntityCapsHashForIdentities:(NSArray*) identities andFeatures:(NSSet*) features andForms:(NSArray*) forms;
+(NSString*) formatLastInteraction:(NSDate*) lastInteraction;
+(NSString*) stringFromTimeInterval:(NSUInteger) interval;
+(NSDate*) parseDateTimeString:(NSString*) datetime;
+(NSString*) generateDateTimeString:(NSDate*) datetime;
+(NSString*) generateRandomPassword;
+(NSString*) encodeRandomResource;
+(NSData* _Nullable) sha1:(NSData* _Nullable) data;
+(NSString* _Nullable) stringSha1:(NSString* _Nullable) data;
+(NSData* _Nullable) sha1HmacForKey:(NSData* _Nullable) key andData:(NSData* _Nullable) data;
+(NSString* _Nullable) stringSha1HmacForKey:(NSString* _Nullable) key andData:(NSString* _Nullable) data;
+(NSData* _Nullable) sha256:(NSData* _Nullable) data;
+(NSString* _Nullable) stringSha256:(NSString* _Nullable) data;
+(NSData* _Nullable) sha256HmacForKey:(NSData* _Nullable) key andData:(NSData* _Nullable) data;
+(NSString* _Nullable) stringSha256HmacForKey:(NSString* _Nullable) key andData:(NSString* _Nullable) data;
+(NSData* _Nullable) sha512:(NSData* _Nullable) data;
+(NSString* _Nullable) stringSha512:(NSString* _Nullable) data;
+(NSData* _Nullable) sha512HmacForKey:(NSData* _Nullable) key andData:(NSData* _Nullable) data;
+(NSString* _Nullable) stringSha512HmacForKey:(NSString* _Nullable) key andData:(NSString* _Nullable) data;
+(NSUUID*) dataToUUID:(NSData*) data;
+(NSUUID*) stringToUUID:(NSString*) data;
+(NSString*) encodeBase64WithString:(NSString*) strData;
+(NSString*) encodeBase64WithData:(NSData*) objData;
+(NSData*) dataWithBase64EncodedString:(NSString*) string;
+(NSString*) hexadecimalString:(NSData*) data;
+(NSData*) dataWithHexString:(NSString*) hex;
+(NSData*) XORData:(NSData*) data1 withData:(NSData*) data2;
+(NSString*) signalHexKeyWithData:(NSData*) data;
+(NSData*) signalIdentityWithHexKey:(NSString*) hexKey;
+(NSString*) signalHexKeyWithSpacesWithData:(NSData*) data;
+(UIView*) MLCustomViewHeaderWithTitle:(NSString*) title;
+(CIImage*) createQRCodeFromString:(NSString*) input;
+(AnyPromise*) waitAtLeastSeconds:(NSTimeInterval) seconds forPromise:(AnyPromise*) promise;
+(NSString*) sanitizeFilePath:(const char* const) file;
//don't use these four directly, but via createTimer() makro
+(MLDelayableTimer*) startDelayableQueuedTimer:(double) timeout withHandler:(monal_void_block_t) handler andCancelHandler:(monal_void_block_t _Nullable) cancelHandler andFile:(char*) file andLine:(int) line andFunc:(char*) func onQueue:(dispatch_queue_t _Nullable) queue;
+(monal_void_block_t) startQueuedTimer:(double) timeout withHandler:(monal_void_block_t) handler andCancelHandler:(monal_void_block_t _Nullable) cancelHandler andFile:(char*) file andLine:(int) line andFunc:(char*) func onQueue:(dispatch_queue_t _Nullable) queue;
+(NSString*) appBuildVersionInfoFor:(MLVersionType) type;
+(NSNumber*) currentTimestampInSeconds;
+(NSNumber*) dateToNSNumberSeconds:(NSDate*) date;
+(BOOL) constantTimeCompareAttackerString:(NSString* _Nonnull) str1 withKnownString:(NSString* _Nonnull) str2;
+(BOOL) isIP:(NSString*) host;
+(NSURLSession*) createEphemeralURLSession;
+(void) updateCurrentLogfilePath:(NSString*) logfilePath;
@end
NS_ASSUME_NONNULL_END

3064
Monal/Classes/HelperTools.m Normal file

File diff suppressed because it is too large Load diff

33
Monal/Classes/IPC.h Normal file
View file

@ -0,0 +1,33 @@
//
// IPC.h
// Monal
//
// Created by Thilo Molitor on 31.07.20.
// Copyright © 2020 Monal.im. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "MLConstants.h"
#define kMonalIncomingIPC @"kMonalIncomingIPC"
NS_ASSUME_NONNULL_BEGIN
typedef void (^IPC_response_handler_t)(NSDictionary*);
@interface IPC : NSObject
+(void) initializeForProcess:(NSString*) processName;
+(id) sharedInstance;
+(void) terminate;
-(void) sendMessage:(NSString*) name withData:(NSData* _Nullable) data to:(NSString*) destination;
-(void) sendMessage:(NSString*) name withData:(NSData* _Nullable) data to:(NSString*) destination withResponseHandler:(IPC_response_handler_t _Nullable) responseHandler;
-(void) sendBroadcastMessage:(NSString*) name withData:(NSData* _Nullable) data;
-(void) sendBroadcastMessage:(NSString*) name withData:(NSData* _Nullable) data withResponseHandler:(IPC_response_handler_t _Nullable) responseHandler;
-(NSString* _Nullable) exportDB;
-(void) respondToMessage:(NSDictionary*) message withData:(NSData* _Nullable) data;
@end
NS_ASSUME_NONNULL_END

331
Monal/Classes/IPC.m Executable file
View file

@ -0,0 +1,331 @@
//
// IPC.m
// Monal
//
// Created by Thilo Molitor on 31.07.20.
// Copyright © 2020 Monal.im. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <CommonCrypto/CommonDigest.h>
#import <notify.h>
#import "IPC.h"
#import "MLSQLite.h"
#import "HelperTools.h"
#define MSG_TIMEOUT 2.0
@interface IPC()
{
NSString* _processName;
NSString* _dbFile;
NSMutableDictionary* _ipcQueues;
NSCondition* _serverThreadCondition;
}
@property (readonly, strong) MLSQLite* db;
@property (readonly, strong) NSThread* serverThread;
-(void) incomingDarwinNotification:(NSString*) name;
@end
static volatile NSMutableDictionary* _responseHandlers;
static volatile IPC* _sharedInstance;
static volatile CFNotificationCenterRef _darwinNotificationCenterRef;
//forward notifications to the IPC instance that is waiting (the instance running the server thread)
void darwinNotificationCenterCallback(CFNotificationCenterRef center __unused, void* observer, CFNotificationName name, const void* object __unused, CFDictionaryRef userInfo __unused)
{
[(__bridge IPC*)observer incomingDarwinNotification:(__bridge NSString*)name];
}
@implementation IPC
+(void) initializeForProcess:(NSString*) processName
{
@synchronized(self) {
MLAssert(_sharedInstance == nil, @"Please don't call [IPC initialize:@\"processName\" twice!");
_responseHandlers = [NSMutableDictionary new];
_darwinNotificationCenterRef = CFNotificationCenterGetDarwinNotifyCenter();
_sharedInstance = [[self alloc] initWithProcessName:processName]; //has to be last because it starts the thread which needs those global vars
}
}
+(id) sharedInstance
{
@synchronized(self) {
MLAssert(_sharedInstance!=nil, @"Please call [IPC initialize:@\"processName\"] first!");
return _sharedInstance;
}
}
+(void) terminate
{
@synchronized(self) {
//cancel server thread and wake it up to let it terminate properly
if(_sharedInstance.serverThread)
[_sharedInstance.serverThread cancel];
[_sharedInstance->_serverThreadCondition signal];
//deallocate everything
_responseHandlers = nil;
_sharedInstance = nil;
}
}
-(void) sendMessage:(NSString*) name withData:(NSData* _Nullable) data to:(NSString*) destination
{
[self sendMessage:name withData:data to:destination withResponseHandler:nil];
}
-(void) sendMessage:(NSString*) name withData:(NSData* _Nullable) data to:(NSString*) destination withResponseHandler:(IPC_response_handler_t _Nullable) responseHandler
{
NSNumber* id = [self writeIpcMessage:name withData:data andResponseId:[NSNumber numberWithInt:0] to:destination];
//save response handler for later execution (if one is specified)
if(responseHandler)
_responseHandlers[id] = responseHandler;
}
-(void) sendBroadcastMessage:(NSString*) name withData:(NSData* _Nullable) data
{
[self sendMessage:name withData:data to:@"*" withResponseHandler:nil];
}
-(void) sendBroadcastMessage:(NSString*) name withData:(NSData* _Nullable) data withResponseHandler:(IPC_response_handler_t _Nullable) responseHandler
{
[self sendMessage:name withData:data to:@"*" withResponseHandler:responseHandler];
}
-(void) respondToMessage:(NSDictionary*) message withData:(NSData* _Nullable) data
{
[self writeIpcMessage:message[@"name"] withData:data andResponseId:message[@"id"] to:message[@"source"]];
}
-(NSString* _Nullable) exportDB
{
NSFileManager* fileManager = [NSFileManager defaultManager];
NSString* temporaryFilename = [NSString stringWithFormat:@"ipc_%@.db", [[NSProcessInfo processInfo] globallyUniqueString]];
NSString* temporaryFilePath = [NSTemporaryDirectory() stringByAppendingPathComponent:temporaryFilename];
//checkpoint db before copying db file
[self.db checkpointWal];
//this transaction creates a new wal log and makes sure the file copy is atomic/consistent
BOOL success = [self.db boolWriteTransaction:^{
//copy db file to temp file
NSError* error;
[fileManager copyItemAtPath:self->_dbFile toPath:temporaryFilePath error:&error];
if(error)
{
DDLogError(@"Could not copy database to export location!");
return NO;
}
return YES;
}];
if(success)
return temporaryFilePath;
return nil;
}
-(id) initWithProcessName:(NSString*) processName
{
self = [super init];
_dbFile = [[HelperTools getContainerURLForPathComponents:@[@"ipc.sqlite"]] path];
_processName = processName;
_ipcQueues = [NSMutableDictionary new];
_serverThreadCondition = [NSCondition new];
static dispatch_once_t once;
static const int VERSION = 3;
dispatch_once(&once, ^{
BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:_dbFile];
//first command creates initial database if file does not exist
//this can not be used inside a transaction --> turn on WAL mode before executing any other db operations
//this will create the database file and open the database because it is the first MLSQlite call done for this file
//turning on WAL mode has to be done *outside* of any transactions
[self.db enableWAL];
[self.db executeNonQuery:@"PRAGMA secure_delete=on;"];
//needed for sqlite >= 3.26.0 (see https://sqlite.org/lang_altertable.html point 2)
[self.db executeNonQuery:@"PRAGMA legacy_alter_table=on;"];
[self.db executeNonQuery:@"PRAGMA foreign_keys=off;"];
NSNumber* version = [self.db idWriteTransaction:^{
if(!fileExists)
{
[self.db executeNonQuery:@"CREATE TABLE ipc(id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, name VARCHAR(255), source VARCHAR(255), destination VARCHAR(255), data BLOB, timeout INTEGER NOT NULL DEFAULT 0);"];
[self.db executeNonQuery:@"CREATE TABLE versions(name VARCHAR(255) NOT NULL PRIMARY KEY, version INTEGER NOT NULL);"];
[self.db executeNonQuery:@"INSERT INTO versions (name, version) VALUES('db', '1');"];
}
//upgrade database version if needed
NSNumber* version = (NSNumber*)[self.db executeScalar:@"SELECT version FROM versions WHERE name='db';"];
DDLogInfo(@"IPC db version: %@", version);
if([version integerValue] < 2)
[self.db executeNonQuery:@"ALTER TABLE ipc ADD COLUMN response_to INTEGER NOT NULL DEFAULT 0;"];
//do a vacuum and from now on do it on every db upgrade
if([version integerValue] < 3)
;
//any upgrade done --> update version table and delete all old ipc messages
if([version integerValue] < VERSION)
{
//always truncate ipc table on version upgrade
[self.db executeNonQuery:@"DELETE FROM ipc;"];
[self.db executeNonQuery:@"UPDATE versions SET version=? WHERE name='db';" andArguments:@[[NSNumber numberWithInt:VERSION]]];
DDLogInfo(@"IPC db upgraded to version: %d", VERSION);
}
return version;
}];
if([version integerValue] < VERSION)
[self.db vacuum];
//turn foreign keys on again
//needed for sqlite >= 3.26.0 (see https://sqlite.org/lang_altertable.html point 2)
[self.db executeNonQuery:@"PRAGMA legacy_alter_table=off;"];
[self.db executeNonQuery:@"PRAGMA foreign_keys=on;"];
});
//use a dedicated and very high priority thread to make sure this always runs
_serverThread = [[NSThread alloc] initWithTarget:self selector:@selector(serverThreadMain) object:nil];
//_serverThread.threadPriority = 1.0;
_serverThread.qualityOfService = NSQualityOfServiceUserInteractive;
[_serverThread setName:@"IPCServerThread"];
[_serverThread start];
return self;
}
-(void) serverThreadMain
{
DDLogInfo(@"Now running IPC server for '%@' with thread priority %f...", _processName, [NSThread threadPriority]);
//register darwin notification handler for "im.monal.ipc.wakeup:<process name>" which is used to wake up readNextMessage using the NSCondition
CFNotificationCenterAddObserver(_darwinNotificationCenterRef, (__bridge void*) self, &darwinNotificationCenterCallback, (__bridge CFNotificationName)[NSString stringWithFormat:@"im.monal.ipc.wakeup:%@", _processName], NULL, 0);
CFNotificationCenterAddObserver(_darwinNotificationCenterRef, (__bridge void*) self, &darwinNotificationCenterCallback, (__bridge CFNotificationName)@"im.monal.ipc.wakeup:*", NULL, 0);
while(![[NSThread currentThread] isCancelled])
{
NSDictionary* message = [self readNextMessage]; //this will be blocking
if(!message)
continue;
DDLogDebug(@"Got IPC message: %@", message);
//use a dedicated serial queue for every IPC receiver to maintain IPC message ordering while not blocking other receivers or this serverThread)
NSArray* parts = [message[@"name"] componentsSeparatedByString:@"."];
NSString* queueName = [parts objectAtIndex:0];
if(!queueName || [parts count]<2)
queueName = @"_default";
queueName = [NSString stringWithFormat:@"ipc.queue:%@", queueName];
if(!_ipcQueues[queueName])
_ipcQueues[queueName] = dispatch_queue_create([queueName cStringUsingEncoding:NSUTF8StringEncoding], dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INTERACTIVE, 0));
//handle all responses (don't trigger a kMonalIncomingIPC for responses)
if(message[@"response_to"] && [message[@"response_to"] intValue] > 0)
{
//call response handler if one is present (ignore the spurious response otherwise)
if(_responseHandlers[message[@"response_to"]])
{
IPC_response_handler_t responseHandler = (IPC_response_handler_t)_responseHandlers[message[@"response_to"]];
if(responseHandler)
{
//responses handlers are only valid for the maximum RTT of messages (+ some safety margin)
createTimer(MSG_TIMEOUT*2 + 1, (^{
[_responseHandlers removeObjectForKey:message[@"response_to"]];
}));
dispatch_async(_ipcQueues[queueName], ^{
responseHandler(message);
});
}
}
}
else //publish all non-responses (using the message name as object allows for filtering by ipc message name)
dispatch_async(_ipcQueues[queueName], ^{
[[NSNotificationCenter defaultCenter] postNotificationName:kMonalIncomingIPC object:message[@"name"] userInfo:message];
});
DDLogDebug(@"Handled IPC message: %@", message);
}
//unregister darwin notification handler
CFNotificationCenterRemoveObserver(_darwinNotificationCenterRef, (__bridge void*) self, (__bridge CFNotificationName)[NSString stringWithFormat:@"im.monal.ipc.wakeup:%@", _processName], NULL);
CFNotificationCenterRemoveObserver(_darwinNotificationCenterRef, (__bridge void*) self, (__bridge CFNotificationName)@"im.monal.ipc.wakeup:*", NULL);
DDLogInfo(@"IPC server for '%@' now terminated", _processName);
}
-(void) incomingDarwinNotification:(NSString*) name
{
DDLogDebug(@"Got incoming darwin notification: %@", name);
[_serverThreadCondition signal]; //wake up server thread to process new messages
}
-(NSDictionary*) readNextMessage
{
while(![[NSThread currentThread] isCancelled])
{
NSDictionary* data = [self readIpcMessageFor:_processName];
if(data)
return data;
//wait for wakeup (incoming darwin notification or thread termination)
DDLogVerbose(@"IPC readNextMessage waiting for wakeup via darwin notification");
[_serverThreadCondition wait];
}
return nil; //thread cancelled
}
//this is the getter of our readonly "db" property always returning the thread-local instance of the MLSQLite class
-(MLSQLite*) db
{
//always return thread-local instance of sqlite class (this is important for performance!)
return [MLSQLite sharedInstanceForFile:_dbFile];
}
-(NSDictionary*) readIpcMessageFor:(NSString*) destination
{
return [self.db idWriteTransaction:^{
NSDictionary* retval = nil;
//delete old entries that timed out
NSNumber* timestamp = [HelperTools currentTimestampInSeconds];
[self.db executeNonQuery:@"DELETE FROM ipc WHERE timeout<?;" andArguments:@[timestamp]];
//load a *single* message from table and delete it afterwards
NSArray* rows = [self.db executeReader:@"SELECT * FROM ipc WHERE destination=? OR destination='*' ORDER BY id ASC LIMIT 1;" andArguments:@[destination]];
if([rows count])
{
retval = rows[0];
if(![retval[@"destination"] isEqualToString:@"*"]) //broadcast will be deleted by their timeout value only
[self.db executeNonQuery:@"DELETE FROM ipc WHERE id=?;" andArguments:@[retval[@"id"]]];
}
return retval;
}];
}
-(NSNumber*) writeIpcMessage:(NSString*) name withData:(NSData* _Nullable) data andResponseId:(NSNumber*) responseId to:(NSString*) destination
{
//empty data is default if not specified
if(!data)
data = [NSData new];
DDLogDebug(@"writeIpcMessage:%@ withData:%@ andResponseId:%@ to:%@", name, data, responseId, destination);
NSNumber* id = [self.db idWriteTransaction:^{
//delete old entries that timed out
NSNumber* timestamp = [HelperTools currentTimestampInSeconds];
[self.db executeNonQuery:@"DELETE FROM ipc WHERE timeout<?;" andArguments:@[timestamp]];
//save message to table
NSNumber* timeout = @([timestamp intValue] + MSG_TIMEOUT); //timeout for every message
[self.db executeNonQuery:@"INSERT INTO ipc (name, source, destination, data, timeout, response_to) VALUES(?, ?, ?, ?, ?, ?);" andArguments:@[name, self->_processName, destination, data, timeout, responseId]];
return [self.db lastInsertId];
}];
//send out darwin notification to wake up other processes waiting for IPC
if(![destination isEqualToString:@"*"])
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), (__bridge CFNotificationName)[NSString stringWithFormat:@"im.monal.ipc.wakeup:%@", destination], NULL, NULL, NO);
else
CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), (__bridge CFNotificationName)@"im.monal.ipc.wakeup:*", NULL, NULL, NO);
DDLogDebug(@"Wrote IPC message %@ to database", id);
return id;
}
@end

View file

@ -0,0 +1,165 @@
//
// LoadingOverlay.swift
// Monal
//
// Created by Jan on 21.06.22.
// Copyright © 2022 Monal.im. All rights reserved.
//
//data class for overlay state
class LoadingOverlayState : ObservableObject {
@Published var enabled: Bool
@Published var headline: AnyView
@Published var description: AnyView
init(enabled:Bool = false, headline:AnyView = AnyView(Text("")), description:AnyView = AnyView(Text(""))) {
self.enabled = enabled
self.headline = headline
self.description = description
}
}
//view modifier for overlay
struct LoadingOverlay: ViewModifier {
@ObservedObject var state : LoadingOverlayState
public func body(content: Content) -> some View {
ZStack(alignment: .center) {
Color(UIColor.systemGroupedBackground).ignoresSafeArea()
content
.disabled(state.enabled == true)
.blur(radius:(state.enabled == true ? 3 : 0))
if(state.enabled == true) {
VStack {
state.headline.font(.headline)
state.description.font(.footnote)
ProgressView()
}
.padding(20)
.frame(minWidth: 250, minHeight: 100)
.background(Color.secondary.colorInvert())
.cornerRadius(20)
.transaction { transaction in transaction.animation = nil}
}
}
}
}
//this extension contains the easy-access view modifier
extension View {
func addLoadingOverlay(_ overlay: LoadingOverlayState) -> some View {
modifier(LoadingOverlay(state:overlay))
}
}
func showPromisingLoadingOverlay<T1:View, T2:View>(_ overlay: LoadingOverlayState, headlineView headline: T1, descriptionView description: T2) -> Guarantee<Void> {
return HelperTools.wait(atLeastSeconds:1.0, for:AnyPromise(DispatchQueue.main.async(.promise) {
overlay.headline = AnyView(headline)
overlay.description = AnyView(description)
overlay.enabled = true
//only rerender ui once (not sure if this optimization is really needed, if this is missing, use @Published for member vars of state class)
overlay.objectWillChange.send()
//make sure to really draw the overlay on race conditions
DispatchQueue.main.asyncAfter(deadline: .now() + 0.250) {
overlay.objectWillChange.send()
}
})).toGuarantee()
}
func showPromisingLoadingOverlay<T:StringProtocol>(_ overlay: LoadingOverlayState, headline: T, description: T = "") -> Guarantee<Void> {
return showPromisingLoadingOverlay(overlay, headlineView:Text(headline), descriptionView:Text(description))
}
func showPromisingLoadingOverlay<T1:View, T2:View, U: Thenable>(_ overlay: LoadingOverlayState, headlineView headline: T1, descriptionView description: T2, firstlyClosure: @escaping () -> U) -> Promise<U.T> {
return Promise { seal in
showPromisingLoadingOverlay(overlay, headlineView: headline, descriptionView: description).done {
let _ = firstlyClosure().done { value in
hideLoadingOverlay(overlay)
seal.fulfill(value)
}.catch { error in
hideLoadingOverlay(overlay)
seal.reject(error)
}
}
}
}
func showPromisingLoadingOverlay<T1:View, T2:View, U: PMKFinalizer>(_ overlay: LoadingOverlayState, headlineView headline: T1, descriptionView description: T2, firstlyClosure: @escaping () -> U) -> Guarantee<Void> {
return Guarantee { seal in
showPromisingLoadingOverlay(overlay, headlineView: headline, descriptionView: description).done {
let _ = firstlyClosure().finally {
hideLoadingOverlay(overlay)
seal(())
}
}
}
}
func showPromisingLoadingOverlay<T:StringProtocol, U: Thenable>(_ overlay: LoadingOverlayState, headline: T, description: T = "", firstlyClosure: @escaping () -> U) -> Promise<U.T> {
return showPromisingLoadingOverlay(overlay, headlineView:Text(headline), descriptionView:Text(description), firstlyClosure:firstlyClosure)
}
func showPromisingLoadingOverlay<T:StringProtocol, U: PMKFinalizer>(_ overlay: LoadingOverlayState, headline: T, description: T = "", firstlyClosure: @escaping () -> U) -> Guarantee<Void> {
return showPromisingLoadingOverlay(overlay, headlineView:Text(headline), descriptionView:Text(description), firstlyClosure:firstlyClosure)
}
func showLoadingOverlay<T1:View, T2:View>(_ overlay: LoadingOverlayState, headlineView headline: T1, descriptionView description: T2) {
let _ = showPromisingLoadingOverlay(overlay, headlineView:headline, descriptionView:description)
}
func showLoadingOverlay<T:StringProtocol>(_ overlay: LoadingOverlayState, headline: T, description: T = "") {
let _ = showPromisingLoadingOverlay(overlay, headline:headline, description:description)
}
func showLoadingOverlay<T1:View, T2:View, U: Thenable>(_ overlay: LoadingOverlayState, headlineView headline: T1, descriptionView description: T2, firstlyClosure: @escaping () -> U) {
let _ = showPromisingLoadingOverlay(overlay, headlineView:headline, descriptionView:description, firstlyClosure:firstlyClosure)
}
func showLoadingOverlay<T1:View, T2:View, U: PMKFinalizer>(_ overlay: LoadingOverlayState, headlineView headline: T1, descriptionView description: T2, firstlyClosure: @escaping () -> U) {
let _ = showPromisingLoadingOverlay(overlay, headlineView:headline, descriptionView:description, firstlyClosure:firstlyClosure)
}
func showLoadingOverlay<T:StringProtocol, U: Thenable>(_ overlay: LoadingOverlayState, headline: T, description: T = "", firstlyClosure: @escaping () -> U) {
let _ = showPromisingLoadingOverlay(overlay, headline:headline, description:description, firstlyClosure:firstlyClosure)
}
func showLoadingOverlay<T:StringProtocol, U: PMKFinalizer>(_ overlay: LoadingOverlayState, headline: T, description: T = "", firstlyClosure: @escaping () -> U) {
let _ = showPromisingLoadingOverlay(overlay, headline:headline, description:description, firstlyClosure:firstlyClosure)
}
func hideLoadingOverlay(_ overlay: LoadingOverlayState) {
DispatchQueue.main.async {
overlay.headline = AnyView(Text(""))
overlay.description = AnyView(Text(""))
overlay.enabled = false
//only rerender ui once (not sure if this optimization is really needed, if this is missing, use @Published for member vars of state class)
overlay.objectWillChange.send()
//make sure to really draw the overlay on race conditions
DispatchQueue.main.asyncAfter(deadline: .now() + 0.250) {
overlay.objectWillChange.send()
}
}
}
struct LoadingOverlay_Previews: PreviewProvider {
@StateObject static var overlay1 = LoadingOverlayState(enabled:true, headline:AnyView(Text("Loading")), description:AnyView(Text("More info?")))
@StateObject static var overlay2 = LoadingOverlayState(enabled:true, headline:AnyView(Text("Loading")), description:AnyView(HStack {
Image(systemName: "checkmark")
Text("Doing a lot of work...")
}))
static var previews: some View {
Form {
Text("Entry 1")
Text("Entry 2")
Text("Entry 3")
}
.addLoadingOverlay(overlay1)
Form {
Text("Entry 1")
Text("Entry 2")
Text("Entry 3")
}
.addLoadingOverlay(overlay2)
}
}

View file

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="13771" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait">
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13772"/>
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" reuseIdentifier="AccountCell" id="l5E-LT-0kT" customClass="MLSwitchCell">
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="l5E-LT-0kT" id="DMi-LI-zqm">
<rect key="frame" x="0.0" y="0.0" width="320" height="43.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" misplaced="YES" text="Label" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="r8c-Jr-MKH">
<rect key="frame" x="8" y="11" width="131" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
<nil key="highlightedColor"/>
</label>
<textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" misplaced="YES" contentHorizontalAlignment="left" contentVerticalAlignment="center" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="ZDH-II-Cc8">
<rect key="frame" x="147" y="11" width="165" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<textInputTraits key="textInputTraits"/>
</textField>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="4FD-Uj-Ib9">
<rect key="frame" x="239" y="9" width="51" height="31"/>
</switch>
</subviews>
<constraints>
<constraint firstItem="ZDH-II-Cc8" firstAttribute="trailing" secondItem="DMi-LI-zqm" secondAttribute="trailingMargin" id="LTg-m3-oEl"/>
<constraint firstItem="ZDH-II-Cc8" firstAttribute="leading" secondItem="r8c-Jr-MKH" secondAttribute="trailing" constant="8" id="WTa-SO-XVZ"/>
<constraint firstItem="4FD-Uj-Ib9" firstAttribute="trailing" secondItem="DMi-LI-zqm" secondAttribute="trailingMargin" constant="-16" id="ai5-3X-WHK"/>
<constraint firstItem="r8c-Jr-MKH" firstAttribute="top" secondItem="DMi-LI-zqm" secondAttribute="topMargin" constant="3" id="dkM-Bb-bcG"/>
<constraint firstItem="r8c-Jr-MKH" firstAttribute="leading" secondItem="DMi-LI-zqm" secondAttribute="leadingMargin" id="eLK-Ws-Cos"/>
<constraint firstItem="ZDH-II-Cc8" firstAttribute="top" secondItem="DMi-LI-zqm" secondAttribute="topMargin" constant="3" id="gL0-U4-7jo"/>
<constraint firstItem="4FD-Uj-Ib9" firstAttribute="top" secondItem="DMi-LI-zqm" secondAttribute="topMargin" constant="-2" id="qN3-vA-V8h"/>
</constraints>
</tableViewCellContentView>
<connections>
<outlet property="cellLabel" destination="r8c-Jr-MKH" id="EYp-Bu-pMd"/>
<outlet property="textInputField" destination="ZDH-II-Cc8" id="XuF-cf-PQ0"/>
<outlet property="toggleSwitch" destination="4FD-Uj-Ib9" id="01q-Cf-jRH"/>
</connections>
<point key="canvasLocation" x="225" y="50"/>
</tableViewCell>
</objects>
</document>

View file

@ -0,0 +1,20 @@
//
// MLAccountPickerViewController.h
// Monal
//
// Created by Anurodh Pokharel on 2/10/20.
// Copyright © 2020 Monal.im. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "MLConstants.h"
NS_ASSUME_NONNULL_BEGIN
@interface MLAccountPickerViewController : UITableViewController
@property (nonatomic, strong) accountCompletion completion;
@end
NS_ASSUME_NONNULL_END

View file

@ -0,0 +1,49 @@
//
// MLAccountPickerViewController.m
// Monal
//
// Created by Anurodh Pokharel on 2/10/20.
// Copyright © 2020 Monal.im. All rights reserved.
//
#import "MLAccountPickerViewController.h"
#import "MLXMPPManager.h"
#import "xmpp.h"
@interface MLAccountPickerViewController ()
@end
@implementation MLAccountPickerViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [[MLXMPPManager sharedInstance].connectedXMPP count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"AccountCell" forIndexPath:indexPath];
xmpp* xmppAccount = [MLXMPPManager sharedInstance].connectedXMPP[indexPath.row];
cell.textLabel.text=xmppAccount.connectionProperties.identity.jid;
return cell;
}
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
if(self.completion) self.completion(indexPath.row);
[self.navigationController popViewControllerAnimated:YES];
}
@end

View file

@ -0,0 +1,19 @@
//
// MLAttributedLabel.h
// Monal
//
// Created by Friedrich Altheide on 01.04.20.
// Copyright © 2020 Monal.im. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface MLAttributedLabel : UILabel
@property (nonatomic, strong) NSAttributedString* localAttributedText;
-(void) setText:(NSString*) text;
-(void) setAttributedText: (NSAttributedString*) attributedText;
-(NSAttributedString*) attributedText;
-(NSAttributedString*) originalAttributedText;
@end

View file

@ -0,0 +1,31 @@
//
// MLAttributedLabel.m
// Monal
//
// Created by Friedrich Altheide on 01.04.20.
// Copyright © 2020 Monal.im. All rights reserved.
//
#import "MLAttributedLabel.h"
@implementation MLAttributedLabel
-(void) setText:(NSString*) text {
self.localAttributedText = nil;
[super setText:text];
}
-(void) setAttributedText:(NSAttributedString*) attributedText {
[super setAttributedText:attributedText];
self.localAttributedText = attributedText;
}
-(NSAttributedString *) attributedText {
return [super attributedText];
}
-(NSAttributedString *) originalAttributedText {
return self.localAttributedText;
}
@end

View file

@ -0,0 +1,37 @@
//
// MLAudioRecoderManager.h
// Monal
//
// Created by jimtsai (poormusic2001@gmail.com) on 2021/2/26.
// Copyright © 2021 Monal.im. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <AVFoundation/AVFoundation.h>
#import "HelperTools.h"
NS_ASSUME_NONNULL_BEGIN
@protocol AudioRecoderManagerDelegate
-(void) notifyResult:(BOOL) isSuccess error:(NSString* _Nullable) errorMsg;
-(void) notifyStart;
-(void) notifyStop:(NSURL* _Nullable) fileURL;
-(void) updateCurrentTime:(NSTimeInterval) audioDuration;
@end
@interface MLAudioRecoderManager : NSObject <AVAudioRecorderDelegate>
@property (strong, nonatomic) AVAudioRecorder* audioRecorder;
@property (weak, nonatomic) id<AudioRecoderManagerDelegate> recoderManagerDelegate;
+ (MLAudioRecoderManager* _Nonnull)sharedInstance;
-(void) start;
-(void) stop:(BOOL) shouldSend;
@property (nonatomic) NSString* currentPlayFilePath;
@end
NS_ASSUME_NONNULL_END

View file

@ -0,0 +1,138 @@
//
// MLAudioRecoderManager.m
// Monal
//
// Created by jimtsai (poormusic2001@gmail.com) on 2021/2/26.
// Copyright © 2021 Monal.im. All rights reserved.
//
#import "MLAudioRecoderManager.h"
NSTimer *updateTimer = nil;
NSURL *audioFileURL = nil;
@implementation MLAudioRecoderManager
+(MLAudioRecoderManager*)sharedInstance
{
static dispatch_once_t once;
static MLAudioRecoderManager* sharedInstance;
dispatch_once(&once, ^{
sharedInstance = [MLAudioRecoderManager new] ;
});
return sharedInstance;
}
-(void) start
{
id<AudioRecoderManagerDelegate> recoderManagerDelegate = self.recoderManagerDelegate;
NSError *audioSessionCategoryError = nil;
NSError *audioRecodSetActiveError = nil;
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
[audioSession setCategory:AVAudioSessionCategoryRecord error:&audioSessionCategoryError];
[audioSession setActive:YES error:&audioRecodSetActiveError];
if (audioSessionCategoryError) {
DDLogError(@"Audio Recorder set category error: %@", audioSessionCategoryError);
[recoderManagerDelegate notifyResult:NO error:NSLocalizedString(@"Audio Recorder set category error: %@", audioSessionCategoryError)];
return;
}
if (audioRecodSetActiveError) {
DDLogError(@"Audio Recorder set active error: %@", audioRecodSetActiveError);
[recoderManagerDelegate notifyResult:NO error:NSLocalizedString(@"Audio Recorder set active error: %@", audioRecodSetActiveError)];
return;
}
NSError* recoderError = nil;
NSDictionary* recodSettings = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithInt:kAudioFormatMPEG4AAC] , AVFormatIDKey,
[NSNumber numberWithInt:AVAudioQualityMin],AVEncoderAudioQualityKey,
[NSNumber numberWithInt: 1], AVNumberOfChannelsKey,
[NSNumber numberWithFloat:32000.0], AVSampleRateKey, nil];
audioFileURL = [NSURL fileURLWithPath:[self getAudioPath]];
self.audioRecorder = [[AVAudioRecorder alloc] initWithURL:audioFileURL settings:recodSettings error:&recoderError];
if(recoderError)
{
DDLogError(@"recorderError: %@", recoderError);
[recoderManagerDelegate notifyResult:NO error:NSLocalizedString(@"Audio recorder init fail.", @"")];
return;
}
self.audioRecorder.delegate = self;
BOOL isPrepare = [self.audioRecorder prepareToRecord];
if(!isPrepare)
{
[recoderManagerDelegate notifyResult:NO error:NSLocalizedString(@"Audio recorder prepareToRecord fail.", @"")];
return;
}
BOOL isRecord = [self.audioRecorder record];
if(!isRecord)
{
[recoderManagerDelegate notifyResult:NO error:NSLocalizedString(@"Audio recorder record fail.", @"")];
return;
}
[recoderManagerDelegate notifyStart];
updateTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(updateTimeInfo) userInfo:nil repeats:YES];
}
-(void) stop:(BOOL) shouldSend
{
self.audioRecorder.delegate = nil;
if(shouldSend)
self.audioRecorder.delegate = self;
[self.audioRecorder stop];
[updateTimer invalidate];
updateTimer = nil;
[self.recoderManagerDelegate notifyStop:shouldSend ? audioFileURL : nil];
if(!shouldSend)
{
NSFileManager* fileManager = [NSFileManager defaultManager];
[fileManager removeItemAtURL:audioFileURL error:nil];
[self.recoderManagerDelegate notifyResult:NO error:NSLocalizedString(@"Aborted recording audio", @"")];
}
}
-(void) updateTimeInfo
{
[self.recoderManagerDelegate updateCurrentTime:self.audioRecorder.currentTime];
}
- (void) audioRecorderDidFinishRecording:(AVAudioRecorder*) recorder successfully:(BOOL) flag
{
id<AudioRecoderManagerDelegate> recoderManagerDelegate = self.recoderManagerDelegate;
if(flag)
{
[recoderManagerDelegate notifyResult:YES error:nil];
}
else
{
[recoderManagerDelegate notifyResult:NO error:NSLocalizedString(@"Audio Recorder: failed to record", @"")];
DDLogError(@"Audio Recorder record fail");
}
}
-(void) audioRecorderEncodeErrorDidOccur:(AVAudioRecorder*) recorder error:(NSError*) error
{
DDLogError(@"Audio Recorder EncodeError: %@", [error description]);
[self.recoderManagerDelegate notifyResult:NO error:[error description]];
}
-(NSString*) getAudioPath
{
NSFileManager* fileManager = [NSFileManager defaultManager];
NSString* writablePath = [[HelperTools getContainerURLForPathComponents:@[@"AudioRecordCache"]] path];
NSError* error = nil;
[fileManager createDirectoryAtPath:writablePath withIntermediateDirectories:YES attributes:nil error:&error];
if(error)
DDLogError(@"Audio Recorder create directory fail: %@", [error description]);
[HelperTools configureFileProtectionFor:writablePath];
NSString* audioFilePath = [writablePath stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.m4a",[[NSUUID UUID] UUIDString]]];
return audioFilePath;
}
@end

View file

@ -0,0 +1,60 @@
//
// MLBaseCell.h
// Monal
//
// Created by Anurodh Pokharel on 12/24/17.
// Copyright © 2017 Monal.im. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "MLConstants.h"
#import "MLMessage.h"
#define kDefaultTextHeight 20
#define kDefaultTextOffset 5
#define kSending NSLocalizedString(@"Sending...", @"")
#define kSent LocalizationNotNeeded(@"")
#define kReceived LocalizationNotNeeded(@"✓")
#define kDisplayed LocalizationNotNeeded(@"✓✓")
@interface MLBaseCell : UITableViewCell
-(id) init;
@property (nonatomic, assign) BOOL outBound;
@property (nonatomic, assign) BOOL MUC;
@property (nonatomic, strong) IBOutlet UILabel* name;
@property (nonatomic, strong) IBOutlet NSLayoutConstraint *nameHeight;
@property (nonatomic, strong) IBOutlet UILabel* date;
@property (nonatomic, strong) IBOutlet UILabel* messageBody;
@property (nonatomic, strong) IBOutlet UILabel* messageStatus;
@property (nonatomic, strong) IBOutlet UILabel* dividerDate;
@property (nonatomic, strong) IBOutlet NSLayoutConstraint *dividerHeight;
@property (nonatomic, strong) IBOutlet NSLayoutConstraint *bubbleTop;
@property (nonatomic, strong) IBOutlet NSLayoutConstraint *dayTop;
@property (nonatomic, strong) NSString* link;
@property (nonatomic, strong) IBOutlet UIView* bubbleView;
@property (nonatomic, weak) IBOutlet UIImageView *bubbleImage;
@property (nonatomic, weak) IBOutlet UIImageView *lockImage;
@property (nonatomic, assign) BOOL deliveryFailed;
@property (nonatomic, strong) IBOutlet UIButton* retry;
@property (nonatomic, strong) NSNumber* messageHistoryId;
@property (nonatomic, weak) UIViewController *parent;
/**
Updates ths cells spacing and display
@param newSender determines if the sender of this cell
is the same as the prior cell's sender
**/
-(void) updateCellWithNewSender:(BOOL) newSender;
-(void) initCell:(MLMessage*) message;
@end

110
Monal/Classes/MLBaseCell.m Normal file
View file

@ -0,0 +1,110 @@
//
// MLBaseCell.m
// Monal
//
// Created by Anurodh Pokharel on 12/24/17.
// Copyright © 2017 Monal.im. All rights reserved.
//
#import "HelperTools.h"
#import "MLBaseCell.h"
#import "MLMessage.h"
@implementation MLBaseCell
-(id) init
{
self = [super init];
[self setRetryButtonImage];
return self;
}
-(void) initCell:(MLMessage*) message
{
[self setRetryButtonImage];
self.messageHistoryId = message.messageDBId;
self.messageBody.text = message.messageText;
self.outBound = !message.inbound;
}
-(void) setRetryButtonImage
{
[self.retry setImage:[UIImage systemImageNamed:@"info.circle"] forState:UIControlStateNormal];
}
- (void)awakeFromNib {
[super awakeFromNib];
BOOL backgrounds = [[HelperTools defaultsDB] boolForKey:@"ChatBackgrounds"];
if(backgrounds) {
self.name.textColor=[UIColor whiteColor];
self.date.textColor=[UIColor whiteColor];
self.messageStatus.textColor=[UIColor whiteColor];
self.dividerDate.textColor=[UIColor whiteColor];
}
}
- (void)setSelected:(BOOL)selected animated:(BOOL)animated {
[super setSelected:selected animated:animated];
// Configure the view for the selected state
}
-(void) updateCellWithNewSender:(BOOL) newSender
{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
if([self.parent respondsToSelector:@selector(retry:)]) {
[self.retry addTarget:self.parent action:@selector(retry:) forControlEvents:UIControlEventTouchUpInside];
}
#pragma clang diagnostic pop
self.retry.tag= [self.messageHistoryId integerValue];
if(self.deliveryFailed) {
self.retry.hidden=NO;
}
else{
self.retry.hidden=YES;
}
if(self.name) {
if(self.name.text.length==0) {
self.nameHeight.constant=0;
self.bubbleTop.constant=0;
self.dayTop.constant=0;
} else {
self.nameHeight.constant= kDefaultTextHeight;
self.bubbleTop.constant=kDefaultTextOffset;
self.dayTop.constant=kDefaultTextOffset;
}
}
if(self.dividerDate.text.length==0) {
self.dividerHeight.constant=0;
if(!self.name) {
self.bubbleTop.constant=0;
self.dayTop.constant=0;
}
} else {
if(!self.name) {
self.bubbleTop.constant=kDefaultTextOffset;
self.dayTop.constant=kDefaultTextOffset;
}
self.dividerHeight.constant=kDefaultTextHeight;
}
if(newSender && self.dividerHeight.constant==0) {
self.dividerHeight.constant= kDefaultTextHeight/2;
}
}
-(void)prepareForReuse{
[super prepareForReuse];
self.deliveryFailed=NO;
self.outBound=NO;
}
@end

View file

@ -0,0 +1,30 @@
//
// MLBasePaser.h
// monalxmpp
//
// Created by Anurodh Pokharel on 4/11/20.
// Copyright © 2020 Monal.im. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "MLXMLNode.h"
//stanzas
#import "XMPPIQ.h"
#import "XMPPPresence.h"
#import "XMPPMessage.h"
#import "XMPPDataForm.h"
NS_ASSUME_NONNULL_BEGIN
typedef void (^stanza_completion_t)(MLXMLNode* _Nullable parsedStanza);
@interface MLBasePaser : NSObject <NSXMLParserDelegate>
-(id) initWithCompletion:(stanza_completion_t) completion;
-(void) reset;
@end
NS_ASSUME_NONNULL_END

156
Monal/Classes/MLBasePaser.m Normal file
View file

@ -0,0 +1,156 @@
//
// MLBasePaser.m
// monalxmpp
//
// Created by Anurodh Pokharel on 4/11/20.
// Copyright © 2020 Monal.im. All rights reserved.
//
#import "MLConstants.h"
#import "MLBasePaser.h"
//#define DebugParser(...) DDLogDebug(__VA_ARGS__)
#define DebugParser(...)
@interface MLXMLNode()
@property (atomic, readwrite) MLXMLNode* parent;
-(MLXMLNode*) addChildNodeWithoutCopy:(MLXMLNode*) child;
@end
@interface MLBasePaser ()
{
//this stak is needed to hold strong references to all nodes until they are dispatched to our _completion callback
//(the parent references of the MLXMLNodes are weak and don't hold the parents alive)
NSMutableArray* _currentStack;
stanza_completion_t _completion;
NSMutableArray* _namespacePrefixes;
}
@end
@implementation MLBasePaser
-(id) initWithCompletion:(stanza_completion_t) completion
{
self = [super init];
_completion = completion;
return self;
}
-(void) reset
{
_currentStack = [NSMutableArray new];
}
-(void) parserDidStartDocument:(NSXMLParser*) parser
{
DDLogInfo(@"Document start");
[self reset];
}
-(void) parser:(NSXMLParser*) parser didStartMappingPrefix:(NSString*) prefix toURI:(NSString*) namespaceURI
{
DebugParser(@"Got new namespace prefix mapping for '%@' to '%@'...", prefix, namespaceURI);
}
-(void) parser:(NSXMLParser*) parser didEndMappingPrefix:(NSString*) prefix
{
DebugParser(@"Namespace prefix '%@' now out of scope again...", prefix);
}
-(void) parser:(NSXMLParser*) parser didStartElement:(NSString*) elementName namespaceURI:(NSString*) namespaceURI qualifiedName:(NSString*) qName attributes:(NSDictionary*) attributeDict
{
NSInteger depth = [_currentStack count] + 1; //this makes the depth in here equal to the depth in didEndElement:
DebugParser(@"Started element: %@ :: %@ (%@) depth %ld", elementName, namespaceURI, qName, depth);
//use appropriate MLXMLNode child classes for iq, message and presence stanzas
MLXMLNode* newNode;
if(depth == 2 && [elementName isEqualToString:@"iq"] && [namespaceURI isEqualToString:@"jabber:client"])
newNode = [XMPPIQ alloc];
else if(depth == 2 && [elementName isEqualToString:@"message"] && [namespaceURI isEqualToString:@"jabber:client"])
newNode = [XMPPMessage alloc];
else if(depth == 2 && [elementName isEqualToString:@"presence"] && [namespaceURI isEqualToString:@"jabber:client"])
newNode = [XMPPPresence alloc];
else if(depth >= 3 && [elementName isEqualToString:@"x"] && [namespaceURI isEqualToString:@"jabber:x:data"])
newNode = [XMPPDataForm alloc];
else
newNode = [MLXMLNode alloc];
newNode = [newNode initWithElement:elementName andNamespace:namespaceURI withAttributes:attributeDict andChildren:@[] andData:nil];
DebugParser(@"Current stack: %@", _currentStack);
//add new node to tree (each node needs a prototype MLXMLNode element and a mutable string to hold its future
//char data added to the MLXMLNode when the xml element is closed
newNode.parent = [_currentStack lastObject][@"node"];
[_currentStack addObject:@{@"node": newNode, @"charData": [NSMutableString new]}];
}
-(void) parser:(NSXMLParser*) parser foundCharacters:(NSString*) string
{
DebugParser(@"Got new xml character data: '%@'", string);
NSInteger depth = [_currentStack count];
if(depth == 0)
{
DDLogError(@"Got xml character data outside of any element!");
[self fakeStreamError];
return;
}
[[_currentStack lastObject][@"charData"] appendString:string];
DebugParser(@"_currentCharData is now: '%@'", [_currentStack lastObject][@"charData"]);
}
-(void) parser:(NSXMLParser*) parser didEndElement:(NSString*) elementName namespaceURI:(NSString*) namespaceURI qualifiedName:(NSString*) qName
{
NSInteger depth = [_currentStack count];
NSDictionary* topmostStackElement = [_currentStack lastObject];
MLXMLNode* currentNode = ((MLXMLNode*)topmostStackElement[@"node"]);
if([topmostStackElement[@"charData"] length])
currentNode.data = [topmostStackElement[@"charData"] copy];
DebugParser(@"Ended element: %@ :: %@ (%@) depth %ld", elementName, namespaceURI, qName, depth);
MLXMLNode* parent = currentNode.parent;
if(parent)
{
DebugParser(@"Ascending from child %@ to parent %@", currentNode.element, parent.element);
if(depth > 2) //don't add all received stanzas/nonzas as childs to our stream header (that would create a memory leak!)
{
DebugParser(@"Adding %@ to parent %@", currentNode.element, parent.element);
[parent addChildNodeWithoutCopy:currentNode];
}
}
[_currentStack removeLastObject];
//only call completion for stanzas, not for inner elements inside stanzas and not for our outermost stream start element
if(depth == 2)
_completion(currentNode);
}
-(void) parserDidEndDocument:(NSXMLParser*) parser
{
DDLogInfo(@"Document end");
}
-(void) parser:(NSXMLParser*) parser foundIgnorableWhitespace:(NSString*) whitespaceString
{
DebugParser(@"Found ignorable whitespace: '%@'", whitespaceString);
}
-(void) parser:(NSXMLParser*) parser parseErrorOccurred:(NSError*) parseError
{
DDLogError(@"XML parse error occurred: line: %ld , col: %ld desc: %@ ",(long)[parser lineNumber],
(long)[parser columnNumber], [parseError localizedDescription]);
[self fakeStreamError];
}
-(void) fakeStreamError
{
//fake stream error and let xmpp.m handle it
_completion([[MLXMLNode alloc] initWithElement:@"error" andNamespace:@"http://etherx.jabber.org/streams" withAttributes:@{} andChildren:@[
[[MLXMLNode alloc] initWithElement:@"bad-format" andNamespace:@"urn:ietf:params:xml:ns:xmpp-streams" withAttributes:@{} andChildren:@[
[[MLXMLNode alloc] initWithElement:@"text" andNamespace:@"urn:ietf:params:xml:ns:xmpp-streams" withAttributes:@{} andChildren:@[] andData:@"Could not parse XML coming from server"]
] andData:nil]
] andData:nil]);
}
@end

View file

@ -0,0 +1,14 @@
//
// MLButtonCell.h
// Monal
//
// Created by Anurodh Pokharel on 4/10/15.
// Copyright (c) 2015 Monal.im. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface MLButtonCell : UITableViewCell
@property (nonatomic, weak) IBOutlet UILabel *buttonText;
@end

View file

@ -0,0 +1,24 @@
//
// MLButtonCell.m
// Monal
//
// Created by Anurodh Pokharel on 4/10/15.
// Copyright (c) 2015 Monal.im. All rights reserved.
//
#import "MLButtonCell.h"
@implementation MLButtonCell
- (void)awakeFromNib {
[super awakeFromNib];
// Initialization code
}
- (void)setSelected:(BOOL)selected animated:(BOOL)animated {
[super setSelected:selected animated:animated];
// Configure the view for the selected state
}
@end

View file

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14868" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="dark"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14824"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell contentMode="scaleToFill" selectionStyle="default" indentationWidth="10" id="KGk-i7-Jjw" customClass="MLButtonCell">
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Button" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="id4-TH-6AY">
<rect key="frame" x="7" y="10" width="306" height="24"/>
<fontDescription key="fontDescription" type="system" pointSize="20"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstAttribute="centerY" secondItem="id4-TH-6AY" secondAttribute="centerY" id="EAh-KF-kqX"/>
<constraint firstAttribute="centerX" secondItem="id4-TH-6AY" secondAttribute="centerX" constant="-6" id="Ni4-VG-MOl"/>
<constraint firstItem="id4-TH-6AY" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leadingMargin" constant="-8" id="biD-gR-F6A"/>
<constraint firstAttribute="trailingMargin" secondItem="id4-TH-6AY" secondAttribute="trailing" constant="-8" id="i2E-o4-eBj"/>
</constraints>
<variation key="default">
<mask key="constraints">
<exclude reference="Ni4-VG-MOl"/>
</mask>
</variation>
</tableViewCellContentView>
<connections>
<outlet property="buttonText" destination="id4-TH-6AY" id="ytS-Q4-MQR"/>
</connections>
<point key="canvasLocation" x="261" y="275"/>
</tableViewCell>
</objects>
</document>

100
Monal/Classes/MLCall.h Normal file
View file

@ -0,0 +1,100 @@
//
// MLCall.h
// monalxmpp
//
// Created by Thilo Molitor on 30.12.22.
// Copyright © 2022 monal-im.org. All rights reserved.
//
#ifndef MLCall_h
#define MLCall_h
#import <AVFoundation/AVFoundation.h>
NS_ASSUME_NONNULL_BEGIN
@class WebRTCClient;
@protocol RTCVideoRenderer;
@class CXAnswerCallAction;
@class CXEndCallAction;
@class xmpp;
@class MLVoIPProcessor;
@class MLContact;
typedef NS_ENUM(NSUInteger, MLCallType) {
MLCallTypeAudio,
MLCallTypeVideo,
};
typedef NS_ENUM(NSUInteger, MLCallDirection) {
MLCallDirectionIncoming,
MLCallDirectionOutgoing,
};
typedef NS_ENUM(NSUInteger, MLCallState) {
MLCallStateUnknown,
MLCallStateDiscovering,
MLCallStateRinging,
MLCallStateConnecting,
MLCallStateReconnecting,
MLCallStateConnected,
MLCallStateFinished,
};
typedef NS_ENUM(NSUInteger, MLCallFinishReason) {
MLCallFinishReasonUnknown, //dummy default value
MLCallFinishReasonNormal, //used for a call answered and finished locally (call direction etc. don't matter here)
MLCallFinishReasonConnectivityError, //used for a call accepted but not connected (call direction etc. don't matter here)
MLCallFinishReasonSecurityError, //used for a call that could not be encrypted using OMEMO
MLCallFinishReasonUnanswered, //used for a call retracted remotely (always remote party)
MLCallFinishReasonAnsweredElsewhere, //used for a call answered and finished remotely (own account OR remote party)
MLCallFinishReasonRetracted, //used for a call retracted locally (always own acount)
MLCallFinishReasonRejected, //used for a call rejected remotely (own account OR remote party)
MLCallFinishReasonDeclined, //used for a call rejected locally (always own account)
MLCallFinishReasonError, //used for a call error
};
typedef NS_ENUM(NSUInteger, MLCallEncryptionState) {
MLCallEncryptionStateUnknown,
MLCallEncryptionStateClear,
MLCallEncryptionStateToFU,
MLCallEncryptionStateTrusted,
};
@interface MLCall : NSObject
@property (strong, readonly) NSString* description;
@property (nonatomic, strong, readonly) NSUUID* uuid;
@property (nonatomic, strong, readonly) NSString* jmiid;
@property (nonatomic, strong, readonly) MLContact* contact;
@property (nonatomic, readonly) MLCallType callType;
@property (nonatomic, readonly) MLCallDirection direction;
@property (nonatomic, readonly) MLCallEncryptionState encryptionState;
@property (nonatomic, readonly) MLCallState state;
@property (nonatomic, readonly) MLCallFinishReason finishReason;
@property (nonatomic, readonly) uint32_t durationTime;
@property (nonatomic, readonly) BOOL wasConnectedOnce;
@property (nonatomic, assign) BOOL muted;
@property (nonatomic, assign) BOOL speaker;
+(instancetype) makeDummyCall:(int) type;
-(void) end;
//these will not use the correct RTCVideoRenderer protocol like in the implementation because the forward declaration of
//RTCVideoRenderer will not be visible to swift until we have swift 5.9 (feature flag ImportObjcForwardDeclarations) or swift 6.0 support
//see https://github.com/apple/swift-evolution/blob/main/proposals/0384-importing-forward-declared-objc-interfaces-and-protocols.md
-(void) startCaptureLocalVideoWithRenderer:(id) renderer andCameraPosition:(AVCaptureDevicePosition) position;
-(void) stopCaptureLocalVideo;
-(void) renderRemoteVideoWithRenderer:(id) renderer;
-(void) hideVideo;
-(void) showVideo;
-(BOOL) isEqualToContact:(MLContact*) contact;
-(BOOL) isEqualToCall:(MLCall*) call;
-(BOOL) isEqual:(id _Nullable) object;
-(NSUInteger) hash;
@end
NS_ASSUME_NONNULL_END
#endif /* MLCall_h */

1797
Monal/Classes/MLCall.m Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,17 @@
//
// MLChatCell.h
// Monal
//
// Created by Anurodh Pokharel on 8/20/13.
//
//
#import <UIKit/UIKit.h>
#import "MLBaseCell.h"
@interface MLChatCell : MLBaseCell
-(void) openlink: (id) sender;
@end

View file

@ -0,0 +1,82 @@
//
// MLChatCell.m
// Monal
//
// Created by Anurodh Pokharel on 8/20/13.
//
//
#import "MLChatCell.h"
#import "MLImageManager.h"
#import "MLConstants.h"
#import "HelperTools.h"
@import SafariServices;
@implementation MLChatCell
-(void) updateCellWithNewSender:(BOOL) newSender
{
[super updateCellWithNewSender:newSender];
if(self.outBound)
{
self.textLabel.textColor = [UIColor whiteColor];
self.bubbleImage.image = [[MLImageManager sharedInstance] outboundImage];
}
else
{
self.textLabel.textColor = [UIColor blackColor];
self.bubbleImage.image = [[MLImageManager sharedInstance] inboundImage];
}
}
-(BOOL) canPerformAction:(SEL) action withSender:(id) sender
{
if(action == @selector(openlink:))
{
if(self.link)
return YES;
}
return (action == @selector(copy:));
}
-(void) openlink:(id) sender {
if(self.link)
{
NSURL* url = [NSURL URLWithString:self.link];
DDLogInfo(@"Opening link (inline=%@): %@", bool2str([[HelperTools defaultsDB] boolForKey: @"useInlineSafari"]), url);
if([[HelperTools defaultsDB] boolForKey: @"useInlineSafari"] && ([url.scheme.lowercaseString isEqualToString:@"http"] || [url.scheme.lowercaseString isEqualToString:@"https"]))
{
SFSafariViewController* safariView = [[SFSafariViewController alloc] initWithURL:url];
[self.parent presentViewController:safariView animated:YES completion:nil];
}
else
[[UIApplication sharedApplication] performSelector:@selector(openURL:) withObject:url];
}
}
-(void) copy:(id) sender {
UIPasteboard* pboard = [UIPasteboard generalPasteboard];
pboard.string = self.messageBody.text;
}
-(void) prepareForReuse
{
[super prepareForReuse];
self.messageBody.attributedText = nil;
self.messageBody.text = @"";
}
- (void)setSelected:(BOOL) selected animated:(BOOL) animated
{
[super setSelected:selected animated:animated];
// Configure the view for the selected state
}
@end

View file

@ -0,0 +1,20 @@
//
// MLChatImageCell.h
// Monal
//
// Created by Anurodh Pokharel on 12/24/17.
// Copyright © 2017 Monal.im. All rights reserved.
//
#import "MLBaseCell.h"
@class MLMessage;
@interface MLChatImageCell : MLBaseCell
-(void) initCellWithMLMessage:(MLMessage*) message;
-(UIImage*) getDisplayedImage;
@end

View file

@ -0,0 +1,157 @@
//
// MLChatImageCell.m
// Monal
//
// Created by Anurodh Pokharel on 12/24/17.
// Copyright © 2017 Monal.im. All rights reserved.
//
#import "FLAnimatedImage.h"
#import "MLChatImageCell.h"
#import "MLImageManager.h"
#import "MLFiletransfer.h"
#import "MLMessage.h"
#import "HelperTools.h"
@import QuartzCore;
@import UIKit;
@interface MLChatImageCell() {
FLAnimatedImageView* _animatedImageView;
}
@property (nonatomic, weak) IBOutlet UIImageView* thumbnailImage;
@property (nonatomic, weak) IBOutlet UIActivityIndicatorView* spinner;
@property (nonatomic, weak) IBOutlet NSLayoutConstraint* imageWidth;
@property (nonatomic, weak) IBOutlet NSLayoutConstraint* imageHeight;
@end
@implementation MLChatImageCell
-(void) awakeFromNib
{
[super awakeFromNib];
// Initialization code
self.thumbnailImage.layer.cornerRadius = 15.0f;
self.thumbnailImage.layer.masksToBounds = YES;
}
// init a image cell if needed
-(void) initCellWithMLMessage:(MLMessage*) message
{
if(_animatedImageView != nil)
[_animatedImageView removeFromSuperview];
// reset image view if we open a new message
if(self.messageHistoryId != message.messageDBId)
self.thumbnailImage.image = nil;
// init base cell
[super initCell:message];
// load image and display it in the UI if needed
[self loadImage:message];
}
/// Load the image from messageText (link) and display it in the UI
-(void) loadImage:(MLMessage*) msg
{
if(_animatedImageView != nil)
[_animatedImageView removeFromSuperview];
if(msg.messageText && self.thumbnailImage.image == nil)
{
[self.spinner startAnimating];
NSDictionary* info = [MLFiletransfer getFileInfoForMessage:msg];
if(info && [info[@"mimeType"] hasPrefix:@"image/gif"])
{
self.link = msg.messageText;
// uses cached file if the file was already downloaded
FLAnimatedImage* image = [FLAnimatedImage animatedImageWithGIFData:[NSData dataWithContentsOfFile:info[@"cacheFile"]]];
if(!image)
return;
_animatedImageView = [FLAnimatedImageView new];
DDLogVerbose(@"image %@\n--> %fx%f", info, image.size.height, image.size.width);
CGFloat wi = image.size.width;
CGFloat hi = image.size.height;
CGFloat ws = 225.0;
CGFloat hs = 200.0;
CGFloat ri = wi / hi;
CGFloat rs = ws / hs;
if(rs > ri)
_animatedImageView.frame = CGRectMake(0.0, 0.0, wi * hs/hi, hs);
else
_animatedImageView.frame = CGRectMake(0.0, 0.0, ws, hi * ws/wi);
self.imageWidth.constant = _animatedImageView.frame.size.width;
self.imageHeight.constant = _animatedImageView.frame.size.height;
_animatedImageView.animatedImage = image;
[self.thumbnailImage addSubview:_animatedImageView];
self.thumbnailImage.contentMode = UIViewContentModeScaleAspectFit;
}
else if(info && [info[@"mimeType"] hasPrefix:@"image/"])
{
self.link = msg.messageText;
AnyPromise* imagePromise = nil;
// this code already runs in the main queue --> we can't use PMKHang
if([info[@"mimeType"] hasPrefix:@"image/svg"])
imagePromise = [HelperTools renderUIImageFromSVGURL:[NSURL fileURLWithPath:info[@"cacheFile"]]];
else
imagePromise = [AnyPromise promiseWithValue:[[UIImage alloc] initWithContentsOfFile:info[@"cacheFile"]]];
imagePromise.then(^(UIImage* image) {
if(!nilExtractor(image))
return;
DDLogVerbose(@"image %@\n--> %fx%f", info, image.size.height, image.size.width);
CGFloat wi = image.size.width;
CGFloat hi = image.size.height;
CGFloat ws = 225.0;
CGFloat hs = 200.0;
CGFloat ri = wi / hi;
CGFloat rs = ws / hs;
if(rs > ri)
self.thumbnailImage.frame = CGRectMake(0.0, 0.0, wi * hs/hi, hs);
else
self.thumbnailImage.frame = CGRectMake(0.0, 0.0, ws, hi * ws/wi);
self.imageWidth.constant = self.thumbnailImage.frame.size.width;
self.imageHeight.constant = self.thumbnailImage.frame.size.height;
[self.thumbnailImage setImage:image];
}).catch(^(NSError* error) {
DDLogWarn(@"Image promise returned an error: %@", error);
});
}
else
unreachable();
[self.spinner stopAnimating];
}
}
-(UIImage*) getDisplayedImage
{
return self.thumbnailImage.image;
}
-(void) setSelected:(BOOL) selected animated:(BOOL) animated
{
[super setSelected:selected animated:animated];
// Configure the view for the selected state
}
-(BOOL) canPerformAction:(SEL) action withSender:(id) sender
{
return (action == @selector(copy:));
}
-(void) copy:(id) sender
{
UIPasteboard* pboard = [UIPasteboard generalPasteboard];
pboard.image = [self getDisplayedImage];
}
-(void) prepareForReuse
{
[super prepareForReuse];
self.imageHeight.constant = 200;
[self.spinner stopAnimating];
if(_animatedImageView != nil)
[_animatedImageView removeFromSuperview];
}
@end

View file

@ -0,0 +1,27 @@
//
// MLChatInputContainer.h
// Monal
//
// Created by Anurodh Pokharel on 1/20/20.
// Copyright © 2020 Monal.im. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "MLResizingTextView.h"
@protocol ChatInputActionDelegage
@optional
- (void) doScrollDownAction;
@end
NS_ASSUME_NONNULL_BEGIN
@interface MLChatInputContainer : UIView
@property (nonatomic, weak) IBOutlet MLResizingTextView* chatInput;
@property (nonatomic, weak) id <ChatInputActionDelegage> chatInputActionDelegate;
@end
NS_ASSUME_NONNULL_END

View file

@ -0,0 +1,49 @@
//
// MLChatInputContainer.m
// Monal
//
// Created by Anurodh Pokharel on 1/20/20.
// Copyright © 2020 Monal.im. All rights reserved.
//
#import "MLChatInputContainer.h"
@implementation MLChatInputContainer
@synthesize chatInputActionDelegate;
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self) {
self.autoresizingMask = UIViewAutoresizingFlexibleHeight;
self.chatInput.scrollEnabled = NO;
self.chatInput.contentInset = UIEdgeInsetsMake(5, 0, 5, 0);
}
return self;
}
- (CGSize)intrinsicContentSize {
CGSize size = CGSizeMake(self.bounds.size.width, self.chatInput.intrinsicContentSize.height);
return size;
}
- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
return [super hitTest:point withEvent:event];
}
-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
NSArray *subViews = self.subviews;
for(UIView *subView in subViews) {
if (CGRectContainsPoint(subView.frame, point) && subView.frame.origin.y < 0) {
DDLogDebug(@"ScrollDown button tapped...");
//without async dispatch this would do nothing
dispatch_async(dispatch_get_main_queue(), ^{
[self.chatInputActionDelegate doScrollDownAction];
});
}
}
return [super pointInside:point withEvent:event];
}
@end

View file

@ -0,0 +1,23 @@
//
// MLChatMapsCell.h
// Monal
//
// Created by Friedrich Altheide on 29.03.20.
// Copyright © 2020 Monal.im. All rights reserved.
//
#import <MapKit/MapKit.h>
#import "MLBaseCell.h"
@interface MLChatMapsCell : MLBaseCell
@property (nonatomic, weak) IBOutlet MKMapView *map;
@property (nonatomic) CLLocationDegrees longitude;
@property (nonatomic) CLLocationDegrees latitude;
-(void) loadCoordinatesWithCompletion:(void (^)(void))completion;
@end

View file

@ -0,0 +1,68 @@
//
// MLChatMapsCell.m
// Monal
//
// Created by Friedrich Altheide on 29.03.20.
// Copyright © 2020 Monal.im. All rights reserved.
//
#import "MLChatMapsCell.h"
#import "MLImageManager.h"
@import QuartzCore;
@implementation MLChatMapsCell
CLLocationCoordinate2D geoLocation;
- (void)awakeFromNib {
[super awakeFromNib];
// Initialization code
self.map.layer.cornerRadius=15.0f;
self.map.layer.masksToBounds=YES;
}
-(void) loadCoordinatesWithCompletion:(void (^)(void))completion {
// Remove old annotations
[self.map removeAnnotations:self.map.annotations];
geoLocation = CLLocationCoordinate2DMake(self.latitude, self.longitude);
MKPointAnnotation* geoPin = [[MKPointAnnotation alloc]init];
geoPin.coordinate = geoLocation;
[self.map addAnnotation:geoPin];
MKCoordinateRegion viewRegion = MKCoordinateRegionMakeWithDistance(geoLocation, 1500, 1500);
[self.map setRegion:viewRegion animated:FALSE];
// Init tap handling
UITapGestureRecognizer *tapGestureRec = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleMapViewGesture)];
[self.map addGestureRecognizer:tapGestureRec];
completion();
}
-(void) handleMapViewGesture {
NSMutableArray* mapItems = [NSMutableArray new];
MKPlacemark* placemark = [[MKPlacemark alloc]initWithCoordinate:geoLocation];
MKMapItem* location = [[MKMapItem alloc]initWithPlacemark:placemark];
[location setName:@"📍 A Location"];
[mapItems addObject:location];
NSMutableDictionary* launchOptions = [NSMutableDictionary new];
// Open apple maps
[MKMapItem openMapsWithItems:mapItems launchOptions:launchOptions];
}
-(BOOL) canPerformAction:(SEL)action withSender:(id)sender
{
return FALSE;
}
-(void)prepareForReuse{
[super prepareForReuse];
}
@end

View file

@ -0,0 +1,20 @@
//
// MLChatViewHelper.h
// Monal
//
// Created by Friedrich Altheide on 04.08.20.
// Copyright © 2020 Monal.im. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "MLContact.h"
NS_ASSUME_NONNULL_BEGIN
@interface MLChatViewHelper<ClassType> : NSObject
+(void) toggleEncryptionForContact:(MLContact*) contact withSelf:(id) andSelf afterToggle:(void (^)(void)) afterToggle;
@end
NS_ASSUME_NONNULL_END

View file

@ -0,0 +1,36 @@
//
// MLChatViewHelper.m
// Monal
//
// Created by Friedrich Altheide on 04.08.20.
// Copyright © 2020 Monal.im. All rights reserved.
//
#import "MLChatViewHelper.h"
#import "DataLayer.h"
#import "MLContact.h"
@import UIKit.UIAlertController;
@implementation MLChatViewHelper
+(void) toggleEncryptionForContact:(MLContact*) contact withSelf:(id) andSelf afterToggle:(void (^)(void)) afterToggle
{
// Update the encryption value in the caller class
if(![contact toggleEncryption:!contact.isEncrypted])
{
// Show a warning when no device keys could be found and the user tries to enable encryption -> encryption is not possible
UIAlertController* alert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Encryption Not Supported", @"") message:NSLocalizedString(@"This contact does not appear to have any devices that support encryption, please try again later if you think this is wrong.", @"") preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Close", @"") style:UIAlertActionStyleDefault handler:^(UIAlertAction* action __unused) {
[alert dismissViewControllerAnimated:YES completion:nil];
}]];
// open the alert msg in the calling view controller
[andSelf presentViewController:alert animated:YES completion:nil];
}
// Call the code that should update the UI elements
afterToggle();
}
@end

237
Monal/Classes/MLConstants.h Normal file
View file

@ -0,0 +1,237 @@
//
// MLConstants.h
// Monal
//
// Created by Anurodh Pokharel on 7/13/13.
//
//
#import <Foundation/Foundation.h>
#import <TargetConditionals.h>
#import "MLHandler.h"
@import CocoaLumberjack;
#define LOG_FLAG_STDERR (1 << 5)
#define LOG_FLAG_STDOUT (1 << 6)
#define LOG_LEVEL_STDERR (DDLogLevelVerbose | LOG_FLAG_STDERR)
#define LOG_LEVEL_STDOUT (LOG_LEVEL_STDERR | LOG_FLAG_STDOUT)
//behave like DDLogError and flush log on DDLogStderr
#define DDLogStderr(frmt, ...) do { LOG_MAYBE(NO, ddLogLevel, LOG_FLAG_STDERR, 0, nil, __PRETTY_FUNCTION__, frmt, ##__VA_ARGS__); [DDLog flushLog]; } while(0)
#define DDLogStdout(frmt, ...) LOG_MAYBE(NO, ddLogLevel, LOG_FLAG_STDOUT, 0, nil, __PRETTY_FUNCTION__, frmt, ##__VA_ARGS__)
static const DDLogLevel ddLogLevel = LOG_LEVEL_STDOUT;
#import "MLLogFileManager.h"
@import PromiseKit;
#define PMKHangEnum(promise) (((NSNumber*)PMKHang(promise)).integerValue)
#define PMKHangBool(promise) (((NSNumber*)PMKHang(promise)).boolValue)
#define PMKHangInt(promise) (((NSNumber*)PMKHang(promise)).intValue)
#define PMKHangDouble(promise) (((NSNumber*)PMKHang(promise)).doubleValue)
//configure app group constants
#ifdef IS_ALPHA
#define kAppGroup @"group.monalalpha"
#define kMonalOpenURL [NSURL URLWithString:@"monalAlphaOpen://"]
#define kBackgroundProcessingTask @"im.monal.alpha.process"
#define kBackgroundRefreshingTask @"im.monal.alpha.refresh"
#elif defined(IS_QUICKSY)
#define kAppGroup @"group.quicksy"
#define kMonalOpenURL [NSURL URLWithString:@"quicksyOpen://"]
#define kBackgroundProcessingTask @"im.monal.process"
#define kBackgroundRefreshingTask @"im.monal.refresh"
#else
#define kAppGroup @"group.monal"
#define kMonalOpenURL [NSURL URLWithString:@"monalOpen://"]
#define kBackgroundProcessingTask @"im.monal.process"
#define kBackgroundRefreshingTask @"im.monal.refresh"
#endif
#define kMonalKeychainName @"Monal"
//this is in seconds
#if TARGET_OS_MACCATALYST
#define SHORT_PING 4.0
#define LONG_PING 8.0
#define MUC_PING 600
#define BGFETCH_DEFAULT_INTERVAL 3600*1
#else
#define SHORT_PING 4.0
#define LONG_PING 8.0
#define MUC_PING 3600
#define BGFETCH_DEFAULT_INTERVAL 3600*3
#endif
// #define defineBlockType(name, returntype, ...) \
// typedef returntype (^name)(__VA_ARGS__); \
// name _Nonnull castTo_##name(id _Nonnull block) { return block; }
//
// #ifndef blocktypes
// defineBlockType(monal_new_void_block_t, void, void);
// #endif
@class MLContact;
@class MLDelayableTimer;
//some typedefs used throughout the project
typedef void (^contactCompletion)(MLContact* _Nonnull selectedContact) NS_SWIFT_UNAVAILABLE("To be redefined in swift.");
typedef void (^accountCompletion)(NSInteger accountRow) NS_SWIFT_UNAVAILABLE("To be redefined in swift.");
typedef void (^monal_void_block_t)(void) NS_SWIFT_UNAVAILABLE("To be redefined in swift.");
typedef void (^monal_id_block_t)(id _Nonnull) NS_SWIFT_UNAVAILABLE("To be redefined in swift.");
typedef id _Nullable (^monal_id_returning_void_block_t)(void) NS_SWIFT_UNAVAILABLE("To be redefined in swift.");
typedef id _Nullable (^monal_id_returning_id_block_t)(id _Nonnull) NS_SWIFT_UNAVAILABLE("To be redefined in swift.");
typedef void (^monal_upload_completion_t)(NSString* _Nullable url, NSString* _Nullable mimeType, NSNumber* _Nullable size, NSError* _Nullable error) NS_SWIFT_UNAVAILABLE("To be redefined in swift.");
typedef NS_ENUM(NSUInteger, MLAudioState) {
MLAudioStateNormal,
MLAudioStateCall,
};
//some useful macros
#define weakify(var) __weak __typeof__(var) AHKWeak_##var = var
#define strongify(var) _Pragma("clang diagnostic push") _Pragma("clang diagnostic ignored \"-Wshadow\"") __strong __typeof__(var) var = AHKWeak_##var; _Pragma("clang diagnostic pop")
#define nilWrapper(var) (var == nil ? (id)[NSNull null] : (id)var)
#define nilExtractor(var) ((id)var == [NSNull null] ? nil : var)
#define nilDefault(var, def) (var == nil || (id)var == [NSNull null] ? def : var)
#define nilDefaultEnum(var, def) (((NSNumber*)nilDefault(var, def)).integerValue)
#define nilDefaultBool(var, def) (((NSNumber*)nilDefault(var, def)).boolValue)
#define nilDefaultInt(var, def) (((NSNumber*)nilDefault(var, def)).intValue)
#define nilDefaultDouble(var, def) (((NSNumber*)nilDefault(var, def)).doubleValue)
#define emptyDefault(var, eq, def) (var == nil || (id)var == [NSNull null] || [var isEqual:eq] ? def : var)
#define updateIfIdNotEqual(a, b) if(a != b && ![a isEqual:b]) a = b
#define updateIfPrimitiveNotEqual(a, b) if(a != b) a = b
#define var __auto_type
#define let const __auto_type
#define bool2str(b) (b ? @"YES" : @"NO")
#define min(a, b) \
({ __typeof__ (a) _a = (a); \
__typeof__ (b) _b = (b); \
_a < _b ? _a : _b; })
#define max(a, b) \
({ __typeof__ (a) _a = (a); \
__typeof__ (b) _b = (b); \
_a > _b ? _a : _b; })
//make sure we don't define this twice
#ifndef STRIP_PARENTHESES
//see https://stackoverflow.com/a/62984543/3528174
#define STRIP_PARENTHESES(X) __ESC(__ISH X)
#define __ISH(...) __ISH __VA_ARGS__
#define __ESC(...) __ESC_(__VA_ARGS__)
#define __ESC_(...) __VAN ## __VA_ARGS__
#define __VAN__ISH
#endif
// https://clang-analyzer.llvm.org/faq.html#unlocalized_string
__attribute__((annotate("returns_localized_nsstring")))
static inline NSString* _Nonnull LocalizationNotNeeded(NSString* _Nonnull s)
{
return s;
}
#define kServerDoesNotFollowXep0440Error @"__incomplete XEP-0388 support, XEP-0440 MUST be implemented and this mandates that servers MUST at least implement tls-server-end-point__"
//some xmpp related constants
#define kId @"id"
#define kMessageId @"kMessageId"
#define kRegisterNameSpace @"jabber:iq:register"
//all other constants needed
#define kMonalConnectivityChange @"kMonalConnectivityChange"
#define kMonalCallRemoved @"kMonalCallRemoved"
#define kMonalCallAdded @"kMonalCallAdded"
#define kMonalIncomingJMIStanza @"kMonalIncomingJMIStanza"
#define kMonalIncomingVoipCall @"kMonalIncomingVoipCall"
#define kMonalIncomingSDP @"kMonalIncomingSDP"
#define kMonalIncomingICECandidate @"kMonalIncomingICECandidate"
#define kMonalWillBeFreezed @"kMonalWillBeFreezed"
#define kMonalIsFreezed @"kMonalIsFreezed"
#define kMonalNewMessageNotice @"kMonalNewMessageNotice"
#define kMonalMucSubjectChanged @"kMonalMucSubjectChanged"
#define kMonalDeletedMessageNotice @"kMonalDeletedMessageNotice"
#define kMonalDisplayedMessagesNotice @"kMonalDisplayedMessagesNotice"
#define kMonalHistoryMessagesNotice @"kMonalHistoryMessagesNotice"
#define kMLMessageSentToContact @"kMLMessageSentToContact"
#define kMonalSentMessageNotice @"kMonalSentMessageNotice"
#define kMonalMessageFiletransferUpdateNotice @"kMonalMessageFiletransferUpdateNotice"
#define kMonalAccountDiscoDone @"kMonalAccountDiscoDone"
#define kMonalNewPresenceNotice @"kMonalNewPresenceNotice"
#define kMonalLastInteractionUpdatedNotice @"kMonalLastInteractionUpdatedNotice"
#define kMonalMessageReceivedNotice @"kMonalMessageReceivedNotice"
#define kMonalMessageDisplayedNotice @"kMonalMessageDisplayedNotice"
#define kMonalMessageErrorNotice @"kMonalMessageErrorNotice"
#define kMonalReceivedMucInviteNotice @"kMonalReceivedMucInviteNotice"
#define kXMPPError @"kXMPPError"
#define kScheduleBackgroundTask @"kScheduleBackgroundTask"
#define kMonalUpdateUnread @"kMonalUpdateUnread"
#define kMLIsLoggedInNotice @"kMLIsLoggedInNotice"
#define kMLResourceBoundNotice @"kMLResourceBoundNotice"
#define kMonalFinishedCatchup @"kMonalFinishedCatchup"
#define kMonalFinishedOmemoBundleFetch @"kMonalFinishedOmemoBundleFetch"
#define kMonalOmemoStateUpdated @"kMonalOmemoStateUpdated"
#define kMonalUpdateBundleFetchStatus @"kMonalUpdateBundleFetchStatus"
#define kMonalOmemoFetchingStateUpdate @"kMonalOmemoFetchingStateUpdate"
#define kMonalIdle @"kMonalIdle"
#define kMonalFiletransfersIdle @"kMonalFiletransfersIdle"
#define kMonalNotIdle @"kMonalNotIdle"
#define kMonalBackgroundChanged @"kMonalBackgroundChanged"
#define kMLMAMPref @"kMLMAMPref"
#define kMonalAccountStatusChanged @"kMonalAccountStatusChanged"
#define kMonalRefresh @"kMonalRefresh"
#define kMonalContactRefresh @"kMonalContactRefresh"
#define kMonalXmppUserSoftWareVersionRefresh @"kMonalXmppUserSoftWareVersionRefresh"
#define kMonalBlockListRefresh @"kMonalBlockListRefresh"
#define kMonalContactRemoved @"kMonalContactRemoved"
#define kMonalMucParticipantsAndMembersUpdated @"kMonalMucParticipantsAndMembersUpdated"
#define kMucTypeGroup @"group"
#define kMucTypeChannel @"channel"
#define kMucRoleModerator @"moderator"
#define kMucRoleNone @"none"
#define kMucRoleParticipant @"participant"
#define kMucRoleVisitor @"visitor"
#define kMucAffiliationOwner @"owner"
#define kMucAffiliationAdmin @"admin"
#define kMucAffiliationMember @"member"
#define kMucAffiliationOutcast @"outcast"
#define kMucAffiliationNone @"none"
#define kMucActionShowProfile @"profile"
#define kMucActionReinvite @"reinvite"
// max count of char's in a single message (both: sending and receiving)
#define kMonalChatMaxAllowedTextLen 2048
#if TARGET_OS_MACCATALYST
#define kMonalBackscrollingMsgCount 75
#else
#define kMonalBackscrollingMsgCount 50
#endif
//contact cells
#define kusernameKey @"username"
#define kfullNameKey @"fullName"
#define kaccountIDKey @"accountID"
#define kstateKey @"state"
#define kstatusKey @"status"
//info cells
#define kaccountNameKey @"accountName"
#define kinfoTypeKey @"type"
#define kinfoStatusKey @"status"
//use this to completely disable omemo in build
//#ifndef DISABLE_OMEMO
//#define DISABLE_OMEMO 1
//#endif
//build MLXMLNode query statistics (will only optimize MLXMLNode queries if *not* defined)
//#define QueryStatistics 1
#define geoPattern @"^geo:(-?(?:90|[1-8][0-9]|[0-9])(?:\\.[0-9]{1,32})?),(-?(?:180|1[0-7][0-9]|[0-9]{1,2})(?:\\.[0-9]{1,32})?)(;.*)?([?].*)?$"

Some files were not shown because too many files have changed in this diff Show more