copy monal src
173
.gitignore
vendored
|
@ -1,123 +1,74 @@
|
|||
docs
|
||||
Frameworks
|
||||
|
||||
#########################
|
||||
# **.gitignore** file for Xcode4 / OS X Source projects
|
||||
#
|
||||
# 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
|
||||
#########################
|
||||
# rust bridge
|
||||
rust/monal-rust-swift-bridge/generated
|
||||
rust/LibMonalRustSwiftBridge
|
||||
|
||||
#####
|
||||
# OS X temporary files that should never be committed
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/rust
|
||||
# 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
|
||||
*.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/*
|
||||
|
||||
####
|
||||
# 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"
|
||||
|
||||
Monal/build/*
|
||||
*.pbxuser
|
||||
*.mode1v3
|
||||
*.mode2v3
|
||||
*.perspectivev3
|
||||
# NB: also, whitelist the default ones, some projects need to use these
|
||||
!default.pbxuser
|
||||
*.mode1v3
|
||||
!default.mode1v3
|
||||
*.mode2v3
|
||||
!default.mode2v3
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
|
||||
|
||||
####
|
||||
# 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!
|
||||
|
||||
profile
|
||||
*.moved-aside
|
||||
/.idea
|
||||
/ConversationsClassic/.idea
|
||||
/ConversationsClassic.xcodeproj
|
||||
/Info.plist
|
||||
/ConversationsClassic/ConversationsClassic.entitlements
|
||||
/XMPPSwift/Client/VoIP/rickroll.mp4
|
||||
/.nvim
|
||||
/buildServer.json
|
||||
TODO.txt
|
||||
PASSWD.txt
|
||||
DerivedData
|
||||
Monal/.nvim
|
||||
Monal/buildServer.json
|
||||
|
||||
# Pods
|
||||
Monal/Pods
|
||||
|
||||
#Don't accidentally commit localization state
|
||||
Monal/localization/external
|
||||
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
After Width: | Height: | Size: 182 KiB |
BIN
Art/callkit_logo.png
Normal file
After Width: | Height: | Size: 175 KiB |
BIN
Art/chat.png
Normal file
After Width: | Height: | Size: 195 KiB |
BIN
Art/chat2.png
Normal file
After Width: | Height: | Size: 167 KiB |
BIN
Art/chat_dark.png
Normal file
After Width: | Height: | Size: 180 KiB |
BIN
Art/friends.png
Normal file
After Width: | Height: | Size: 174 KiB |
BIN
Art/friends2.png
Normal file
After Width: | Height: | Size: 146 KiB |
BIN
Art/friends_dark.png
Normal file
After Width: | Height: | Size: 190 KiB |
BIN
Art/logo.png
Normal file
After Width: | Height: | Size: 188 KiB |
149
Art/monal.svg
Normal 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
After Width: | Height: | Size: 307 KiB |
BIN
Art/park_colors.png
Normal file
After Width: | Height: | Size: 477 KiB |
BIN
Art/park_white_black.png
Normal file
After Width: | Height: | Size: 358 KiB |
BIN
Art/screenshots/01_groupchats.png
Normal file
After Width: | Height: | Size: 623 KiB |
BIN
Art/screenshots/02_chats.png
Normal file
After Width: | Height: | Size: 502 KiB |
BIN
Art/screenshots/04_contacts.png
Normal file
After Width: | Height: | Size: 617 KiB |
BIN
Art/screenshots/ipad_01_groupchats.png
Normal file
After Width: | Height: | Size: 1.4 MiB |
29
LICENSE
Normal 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
|
@ -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
|
BIN
Monal/AlertSounds/alert1.aif
Normal file
BIN
Monal/AlertSounds/alert10.aif
Normal file
BIN
Monal/AlertSounds/alert11.aif
Normal file
BIN
Monal/AlertSounds/alert12.aif
Normal file
BIN
Monal/AlertSounds/alert2.aif
Normal file
BIN
Monal/AlertSounds/alert3.aif
Normal file
BIN
Monal/AlertSounds/alert4.aif
Normal file
BIN
Monal/AlertSounds/alert5.aif
Normal file
BIN
Monal/AlertSounds/alert6.aif
Normal file
BIN
Monal/AlertSounds/alert7.aif
Normal file
BIN
Monal/AlertSounds/alert8.aif
Normal file
BIN
Monal/AlertSounds/alert9.aif
Normal file
14
Monal/Alpha.shareSheet.entitlements
Normal 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
BIN
Monal/CallSounds/error.wav
Normal file
BIN
Monal/CallSounds/ringing.wav
Normal file
26
Monal/Classes/AESGcm.h
Normal 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
|
@ -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
|
594
Monal/Classes/AVCallUI.swift
Normal 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))
|
||||
}
|
||||
}
|
20
Monal/Classes/AccountListController.h
Normal 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
|
114
Monal/Classes/AccountListController.m
Normal 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
|
66
Monal/Classes/AccountPicker.swift
Normal 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)
|
||||
}
|
||||
}
|
64
Monal/Classes/ActiveChatsViewController.h
Normal 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
|
1558
Monal/Classes/ActiveChatsViewController.m
Executable file
347
Monal/Classes/AddContactMenu.swift
Normal 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
|
||||
})
|
||||
}
|
||||
}
|
100
Monal/Classes/BackgroundSettings.swift
Normal 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)
|
||||
}
|
||||
}
|
119
Monal/Classes/BlockedUsers.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
254
Monal/Classes/BoardingCards.swift
Normal 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())
|
||||
}
|
||||
}
|
77
Monal/Classes/ChannelMemberList.swift
Normal 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)));
|
||||
}
|
||||
}
|
29
Monal/Classes/ChatPlaceholder.swift
Normal 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()
|
||||
}
|
||||
}
|
701
Monal/Classes/ContactDetails.swift
Normal 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)))
|
||||
}
|
||||
}
|
86
Monal/Classes/ContactEntry.swift
Normal 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)))
|
||||
}
|
135
Monal/Classes/ContactPicker.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
120
Monal/Classes/ContactRequestsMenu.swift
Normal 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()
|
||||
}
|
||||
}
|
159
Monal/Classes/ContactResources.swift
Normal 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())
|
||||
}
|
||||
}
|
194
Monal/Classes/ContactsView.swift
Normal 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
|
||||
}
|
||||
}
|
49
Monal/Classes/ContentUnavailableShimView.swift
Normal 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."))
|
||||
}
|
141
Monal/Classes/CreateGroupMenu.swift
Normal 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
|
@ -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
21
Monal/Classes/DataLayerMigrations.h
Normal 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
|
1197
Monal/Classes/DataLayerMigrations.m
Normal file
202
Monal/Classes/DebugView.swift
Normal 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()
|
||||
}
|
||||
}
|
51
Monal/Classes/EditGroupSubject.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
27
Monal/Classes/EncryptedPayload.swift
Normal 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
|
||||
}
|
||||
}
|
536
Monal/Classes/GeneralSettings.swift
Normal 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()
|
||||
}
|
||||
}
|
15
Monal/Classes/HelperTools+Quicksy_CountryCodes.h
Normal 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
|
255
Monal/Classes/HelperTools+Quicksy_CountryCodes.m
Normal 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
|
@ -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
33
Monal/Classes/IPC.h
Normal 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
|
@ -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
|
165
Monal/Classes/LoadingOverlay.swift
Normal 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)
|
||||
}
|
||||
}
|
55
Monal/Classes/MLAccountCell.xib
Normal 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>
|
20
Monal/Classes/MLAccountPickerViewController.h
Normal 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
|
49
Monal/Classes/MLAccountPickerViewController.m
Normal 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
|
19
Monal/Classes/MLAttributedLabel.h
Normal 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
|
31
Monal/Classes/MLAttributedLabel.m
Normal 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
|
37
Monal/Classes/MLAudioRecoderManager.h
Normal 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
|
138
Monal/Classes/MLAudioRecoderManager.m
Normal 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
|
60
Monal/Classes/MLBaseCell.h
Normal 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
|
@ -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
|
30
Monal/Classes/MLBasePaser.h
Normal 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
|
@ -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
|
14
Monal/Classes/MLButtonCell.h
Normal 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
|
24
Monal/Classes/MLButtonCell.m
Normal 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
|
44
Monal/Classes/MLButtonCell.xib
Normal 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
|
@ -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
17
Monal/Classes/MLChatCell.h
Normal 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
|
82
Monal/Classes/MLChatCell.m
Normal 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
|
20
Monal/Classes/MLChatImageCell.h
Normal 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
|
||||
|
157
Monal/Classes/MLChatImageCell.m
Normal 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
|
27
Monal/Classes/MLChatInputContainer.h
Normal 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
|
49
Monal/Classes/MLChatInputContainer.m
Normal 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
|
23
Monal/Classes/MLChatMapsCell.h
Normal 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
|
||||
|
68
Monal/Classes/MLChatMapsCell.m
Normal 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
|
20
Monal/Classes/MLChatViewHelper.h
Normal 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
|
36
Monal/Classes/MLChatViewHelper.m
Normal 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
|
@ -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})?)(;.*)?([?].*)?$"
|