Merge branch 'master' into remote-desktop
This commit is contained in:
commit
3790752378
@ -14,20 +14,28 @@ import PhotosUI
|
||||
struct NativeTextEditor: UIViewRepresentable {
|
||||
@Binding var text: String
|
||||
@Binding var disableEditing: Bool
|
||||
let height: CGFloat
|
||||
let font: UIFont
|
||||
@Binding var height: CGFloat
|
||||
@Binding var focused: Bool
|
||||
let alignment: TextAlignment
|
||||
let onImagesAdded: ([UploadContent]) -> Void
|
||||
|
||||
private let minHeight: CGFloat = 37
|
||||
|
||||
private let defaultHeight: CGFloat = {
|
||||
let field = CustomUITextField(height: Binding.constant(0))
|
||||
field.textContainerInset = UIEdgeInsets(top: 8, left: 5, bottom: 6, right: 4)
|
||||
return min(max(field.sizeThatFits(CGSizeMake(field.frame.size.width, CGFloat.greatestFiniteMagnitude)).height, 37), 360).rounded(.down)
|
||||
}()
|
||||
|
||||
func makeUIView(context: Context) -> UITextView {
|
||||
let field = CustomUITextField()
|
||||
let field = CustomUITextField(height: _height)
|
||||
field.text = text
|
||||
field.font = font
|
||||
field.textAlignment = alignment == .leading ? .left : .right
|
||||
field.autocapitalizationType = .sentences
|
||||
field.setOnTextChangedListener { newText, images in
|
||||
if !disableEditing {
|
||||
// Speed up the process of updating layout, reduce jumping content on screen
|
||||
if !isShortEmoji(newText) { updateHeight(field) }
|
||||
text = newText
|
||||
} else {
|
||||
field.text = text
|
||||
@ -39,24 +47,72 @@ struct NativeTextEditor: UIViewRepresentable {
|
||||
field.setOnFocusChangedListener { focused = $0 }
|
||||
field.delegate = field
|
||||
field.textContainerInset = UIEdgeInsets(top: 8, left: 5, bottom: 6, right: 4)
|
||||
updateFont(field)
|
||||
updateHeight(field)
|
||||
return field
|
||||
}
|
||||
|
||||
func updateUIView(_ field: UITextView, context: Context) {
|
||||
field.text = text
|
||||
field.font = font
|
||||
field.textAlignment = alignment == .leading ? .left : .right
|
||||
updateFont(field)
|
||||
updateHeight(field)
|
||||
}
|
||||
|
||||
private func updateHeight(_ field: UITextView) {
|
||||
let maxHeight = min(360, field.font!.lineHeight * 12)
|
||||
// When having emoji in text view and then removing it, sizeThatFits shows previous size (too big for empty text view), so using work around with default size
|
||||
let newHeight = field.text == ""
|
||||
? defaultHeight
|
||||
: min(max(field.sizeThatFits(CGSizeMake(field.frame.size.width, CGFloat.greatestFiniteMagnitude)).height, minHeight), maxHeight).rounded(.down)
|
||||
|
||||
if field.frame.size.height != newHeight {
|
||||
field.frame.size = CGSizeMake(field.frame.size.width, newHeight)
|
||||
(field as! CustomUITextField).invalidateIntrinsicContentHeight(newHeight)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateFont(_ field: UITextView) {
|
||||
field.font = isShortEmoji(field.text)
|
||||
? (field.text.count < 4 ? largeEmojiUIFont : mediumEmojiUIFont)
|
||||
: UIFont.preferredFont(forTextStyle: .body)
|
||||
}
|
||||
}
|
||||
|
||||
private class CustomUITextField: UITextView, UITextViewDelegate {
|
||||
var height: Binding<CGFloat>
|
||||
var newHeight: CGFloat = 0
|
||||
var onTextChanged: (String, [UploadContent]) -> Void = { newText, image in }
|
||||
var onFocusChanged: (Bool) -> Void = { focused in }
|
||||
|
||||
init(height: Binding<CGFloat>) {
|
||||
self.height = height
|
||||
super.init(frame: .zero, textContainer: nil)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("Not implemented")
|
||||
}
|
||||
|
||||
// This func here needed because using frame.size.height in intrinsicContentSize while loading a screen with text (for example. when you have a draft),
|
||||
// produces incorrect height because at that point intrinsicContentSize has old value of frame.size.height even if it was set to new value right before the call
|
||||
// (who knows why...)
|
||||
func invalidateIntrinsicContentHeight(_ newHeight: CGFloat) {
|
||||
self.newHeight = newHeight
|
||||
invalidateIntrinsicContentSize()
|
||||
}
|
||||
|
||||
override var intrinsicContentSize: CGSize {
|
||||
if height.wrappedValue != newHeight {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now(), execute: { self.height.wrappedValue = self.newHeight })
|
||||
}
|
||||
return CGSizeMake(0, newHeight)
|
||||
}
|
||||
|
||||
func setOnTextChangedListener(onTextChanged: @escaping (String, [UploadContent]) -> Void) {
|
||||
self.onTextChanged = onTextChanged
|
||||
}
|
||||
|
||||
|
||||
func setOnFocusChangedListener(onFocusChanged: @escaping (Bool) -> Void) {
|
||||
self.onFocusChanged = onFocusChanged
|
||||
}
|
||||
@ -144,14 +200,14 @@ private class CustomUITextField: UITextView, UITextViewDelegate {
|
||||
|
||||
struct NativeTextEditor_Previews: PreviewProvider{
|
||||
static var previews: some View {
|
||||
return NativeTextEditor(
|
||||
NativeTextEditor(
|
||||
text: Binding.constant("Hello, world!"),
|
||||
disableEditing: Binding.constant(false),
|
||||
height: 100,
|
||||
font: UIFont.preferredFont(forTextStyle: .body),
|
||||
height: Binding.constant(100),
|
||||
focused: Binding.constant(false),
|
||||
alignment: TextAlignment.leading,
|
||||
onImagesAdded: { _ in }
|
||||
)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
|
@ -32,15 +32,12 @@ struct SendMessageView: View {
|
||||
var sendButtonColor = Color.accentColor
|
||||
@State private var teHeight: CGFloat = 42
|
||||
@State private var teFont: Font = .body
|
||||
@State private var teUiFont: UIFont = UIFont.preferredFont(forTextStyle: .body)
|
||||
@State private var sendButtonSize: CGFloat = 29
|
||||
@State private var sendButtonOpacity: CGFloat = 1
|
||||
@State private var showCustomDisappearingMessageDialogue = false
|
||||
@State private var showCustomTimePicker = false
|
||||
@State private var selectedDisappearingMessageTime: Int? = customDisappearingMessageTimeDefault.get()
|
||||
@State private var progressByTimeout = false
|
||||
var maxHeight: CGFloat = 360
|
||||
var minHeight: CGFloat = 37
|
||||
@AppStorage(DEFAULT_LIVE_MESSAGE_ALERT_SHOWN) private var liveMessageAlertShown = false
|
||||
|
||||
var body: some View {
|
||||
@ -57,30 +54,16 @@ struct SendMessageView: View {
|
||||
.frame(maxWidth: .infinity)
|
||||
} else {
|
||||
let alignment: TextAlignment = isRightToLeft(composeState.message) ? .trailing : .leading
|
||||
Text(composeState.message)
|
||||
.lineLimit(10)
|
||||
.font(teFont)
|
||||
.multilineTextAlignment(alignment)
|
||||
// put text on top (after NativeTextEditor) and set color to precisely align it on changes
|
||||
// .foregroundColor(.red)
|
||||
.foregroundColor(.clear)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.top, 8)
|
||||
.padding(.bottom, 6)
|
||||
.matchedGeometryEffect(id: "te", in: namespace)
|
||||
.background(GeometryReader(content: updateHeight))
|
||||
|
||||
NativeTextEditor(
|
||||
text: $composeState.message,
|
||||
disableEditing: $composeState.inProgress,
|
||||
height: teHeight,
|
||||
font: teUiFont,
|
||||
height: $teHeight,
|
||||
focused: $keyboardVisible,
|
||||
alignment: alignment,
|
||||
onImagesAdded: onMediaAdded
|
||||
)
|
||||
.allowsTightening(false)
|
||||
.frame(height: teHeight)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
|
||||
@ -100,11 +83,13 @@ struct SendMessageView: View {
|
||||
.frame(height: teHeight, alignment: .bottom)
|
||||
}
|
||||
}
|
||||
|
||||
.padding(.vertical, 1)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerSize: CGSize(width: 20, height: 20))
|
||||
.strokeBorder(.secondary, lineWidth: 0.3, antialiased: true)
|
||||
.frame(height: teHeight)
|
||||
)
|
||||
}
|
||||
.onChange(of: composeState.message, perform: { text in updateFont(text) })
|
||||
.onChange(of: composeState.inProgress) { inProgress in
|
||||
if inProgress {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
|
||||
@ -415,16 +400,12 @@ struct SendMessageView: View {
|
||||
.padding([.bottom, .trailing], 4)
|
||||
}
|
||||
|
||||
private func updateHeight(_ g: GeometryProxy) -> Color {
|
||||
private func updateFont(_ text: String) {
|
||||
DispatchQueue.main.async {
|
||||
teHeight = min(max(g.frame(in: .local).size.height, minHeight), maxHeight)
|
||||
(teFont, teUiFont) = isShortEmoji(composeState.message)
|
||||
? composeState.message.count < 4
|
||||
? (largeEmojiFont, largeEmojiUIFont)
|
||||
: (mediumEmojiFont, mediumEmojiUIFont)
|
||||
: (.body, UIFont.preferredFont(forTextStyle: .body))
|
||||
teFont = isShortEmoji(text)
|
||||
? (text.count < 4 ? largeEmojiFont : mediumEmojiFont)
|
||||
: .body
|
||||
}
|
||||
return Color.clear
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,7 @@ constraints: zip +disable-bzip2 +disable-zstd
|
||||
source-repository-package
|
||||
type: git
|
||||
location: https://github.com/simplex-chat/simplexmq.git
|
||||
tag: 55a6157880396be899c010f880a42322cf65258a
|
||||
tag: d920a2504b6d4653748da7d297cb13cd0a0f1f48
|
||||
|
||||
source-repository-package
|
||||
type: git
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"https://github.com/simplex-chat/simplexmq.git"."55a6157880396be899c010f880a42322cf65258a" = "1fhhyi2060pp72izrqki6gazb759hcv9wypxf39jkwpqpvrn81hv";
|
||||
"https://github.com/simplex-chat/simplexmq.git"."d920a2504b6d4653748da7d297cb13cd0a0f1f48" = "0r53wn01z044h6myvd458n3hiqsz64kpv59khgybzwdw5mmqnp34";
|
||||
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
|
||||
"https://github.com/kazu-yamamoto/http2.git"."804fa283f067bd3fd89b8c5f8d25b3047813a517" = "1j67wp7rfybfx3ryx08z6gqmzj85j51hmzhgx47ihgmgr47sl895";
|
||||
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "0kiwhvml42g9anw4d2v0zd1fpc790pj9syg5x3ik4l97fnkbbwpp";
|
||||
|
@ -5887,7 +5887,7 @@ chatCommandP =
|
||||
mcTextP = MCText . safeDecodeUtf8 <$> A.takeByteString
|
||||
msgContentP = "text " *> mcTextP <|> "json " *> jsonP
|
||||
ciDeleteMode = "broadcast" $> CIDMBroadcast <|> "internal" $> CIDMInternal
|
||||
displayName = safeDecodeUtf8 <$> (quoted "'\"" <|> takeNameTill isSpace)
|
||||
displayName = safeDecodeUtf8 <$> (quoted "'" <|> takeNameTill isSpace)
|
||||
where
|
||||
takeNameTill p =
|
||||
A.peekChar' >>= \c ->
|
||||
@ -6002,14 +6002,20 @@ timeItToView s action = do
|
||||
pure a
|
||||
|
||||
mkValidName :: String -> String
|
||||
mkValidName = reverse . dropWhile isSpace . fst . foldl' addChar ("", '\NUL')
|
||||
mkValidName = reverse . dropWhile isSpace . fst3 . foldl' addChar ("", '\NUL', 0 :: Int)
|
||||
where
|
||||
addChar (r, prev) c = if notProhibited && validChar then (c' : r, c') else (r, prev)
|
||||
fst3 (x, _, _) = x
|
||||
addChar (r, prev, punct) c = if validChar then (c' : r, c', punct') else (r, prev, punct)
|
||||
where
|
||||
c' = if isSpace c then ' ' else c
|
||||
punct'
|
||||
| isPunctuation c = punct + 1
|
||||
| isSpace c = punct
|
||||
| otherwise = 0
|
||||
validChar
|
||||
| prev == '\NUL' || isSpace prev = validFirstChar
|
||||
| isPunctuation prev = validFirstChar || isSpace c
|
||||
| c == '\'' = False
|
||||
| prev == '\NUL' = c > ' ' && c /= '#' && c /= '@' && validFirstChar
|
||||
| isSpace prev = validFirstChar || (punct == 0 && isPunctuation c)
|
||||
| isPunctuation prev = validFirstChar || isSpace c || (punct < 3 && isPunctuation c)
|
||||
| otherwise = validFirstChar || isSpace c || isMark c || isPunctuation c
|
||||
validFirstChar = isLetter c || isNumber c || isSymbol c
|
||||
notProhibited = c `notElem` ("@#'\"`" :: String)
|
||||
|
@ -49,7 +49,7 @@ extra-deps:
|
||||
# - simplexmq-1.0.0@sha256:34b2004728ae396e3ae449cd090ba7410781e2b3cefc59259915f4ca5daa9ea8,8561
|
||||
# - ../simplexmq
|
||||
- github: simplex-chat/simplexmq
|
||||
commit: 55a6157880396be899c010f880a42322cf65258a
|
||||
commit: d920a2504b6d4653748da7d297cb13cd0a0f1f48
|
||||
- github: kazu-yamamoto/http2
|
||||
commit: 804fa283f067bd3fd89b8c5f8d25b3047813a517
|
||||
# - ../direct-sqlcipher
|
||||
|
@ -14,14 +14,26 @@ testMkValidName = do
|
||||
mkValidName "John Doe" `shouldBe` "John Doe"
|
||||
mkValidName "J.Doe" `shouldBe` "J.Doe"
|
||||
mkValidName "J. Doe" `shouldBe` "J. Doe"
|
||||
mkValidName "J..Doe" `shouldBe` "J.Doe"
|
||||
mkValidName "J ..Doe" `shouldBe` "J Doe"
|
||||
mkValidName "J . . Doe" `shouldBe` "J Doe"
|
||||
mkValidName "J..Doe" `shouldBe` "J..Doe"
|
||||
mkValidName "J ..Doe" `shouldBe` "J ..Doe"
|
||||
mkValidName "J ... Doe" `shouldBe` "J ... Doe"
|
||||
mkValidName "J .... Doe" `shouldBe` "J ... Doe"
|
||||
mkValidName "J . . Doe" `shouldBe` "J . Doe"
|
||||
mkValidName "@alice" `shouldBe` "alice"
|
||||
mkValidName "#alice" `shouldBe` "alice"
|
||||
mkValidName " alice" `shouldBe` "alice"
|
||||
mkValidName "alice " `shouldBe` "alice"
|
||||
mkValidName "John Doe" `shouldBe` "John Doe"
|
||||
mkValidName "'John Doe'" `shouldBe` "John Doe"
|
||||
mkValidName "\"John Doe\"" `shouldBe` "John Doe"
|
||||
mkValidName "`John Doe`" `shouldBe` "John Doe"
|
||||
mkValidName "\"John Doe\"" `shouldBe` "John Doe\""
|
||||
mkValidName "`John Doe`" `shouldBe` "`John Doe`"
|
||||
mkValidName "John \"Doe\"" `shouldBe` "John \"Doe\""
|
||||
mkValidName "John `Doe`" `shouldBe` "John `Doe`"
|
||||
mkValidName "alice/bob" `shouldBe` "alice/bob"
|
||||
mkValidName "alice / bob" `shouldBe` "alice / bob"
|
||||
mkValidName "alice /// bob" `shouldBe` "alice /// bob"
|
||||
mkValidName "alice //// bob" `shouldBe` "alice /// bob"
|
||||
mkValidName "alice >>= bob" `shouldBe` "alice >>= bob"
|
||||
mkValidName "alice@example.com" `shouldBe` "alice@example.com"
|
||||
mkValidName "alice <> bob" `shouldBe` "alice <> bob"
|
||||
mkValidName "alice -> bob" `shouldBe` "alice -> bob"
|
||||
|
Loading…
Reference in New Issue
Block a user