From cb14a23ec0b39ef477b3dd5b60e8fc6d12733d25 Mon Sep 17 00:00:00 2001 From: Taras Smakula <tarassmakula@gmail.com> Date: Mon, 27 Nov 2023 18:00:11 +0200 Subject: [PATCH] Copy new markdown --- app/build.gradle | 5 +- .../circles/extensions/EditableExtensions.kt | 48 ++ .../java/org/futo/circles/view/Callback.kt | 11 + .../org/futo/circles/view/ComposerEditText.kt | 123 ++++ .../futo/circles/view/DimensionConverter.kt | 46 ++ .../futo/circles/view/MessageComposerMode.kt | 28 + .../futo/circles/view/MessageComposerView.kt | 41 ++ .../futo/circles/view/PillDisplayHandler.kt | 80 +++ .../org/futo/circles/view/PillImageSpan.kt | 141 ++++ .../org/futo/circles/view/PreviewPostView.kt | 62 +- .../circles/view/RichTextComposerLayout.kt | 675 ++++++++++++++++++ .../circles/view/RichTextEditorException.kt | 5 + .../futo/circles/view/SimpleTextWatcher.kt | 37 + .../futo/circles/view/UriContentListener.kt | 45 ++ .../color/selector_rich_text_menu_icon.xml | 8 + app/src/main/res/drawable/bg_code_block.xml | 22 + .../bg_inline_code_multi_line_left.xml | 23 + .../bg_inline_code_multi_line_mid.xml | 21 + .../bg_inline_code_multi_line_right.xml | 23 + .../drawable/bg_inline_code_single_line.xml | 22 + .../res/drawable/bg_rich_text_menu_button.xml | 18 + .../main/res/drawable/ic_composer_bold.xml | 10 + .../res/drawable/ic_composer_bullet_list.xml | 12 + .../res/drawable/ic_composer_code_block.xml | 9 + .../res/drawable/ic_composer_collapse.xml | 9 + .../res/drawable/ic_composer_full_screen.xml | 9 + .../main/res/drawable/ic_composer_indent.xml | 12 + .../res/drawable/ic_composer_inline_code.xml | 15 + .../main/res/drawable/ic_composer_italic.xml | 10 + .../main/res/drawable/ic_composer_link.xml | 12 + .../drawable/ic_composer_numbered_list.xml | 24 + .../main/res/drawable/ic_composer_quote.xml | 18 + .../drawable/ic_composer_rich_mic_pressed.xml | 17 + .../ic_composer_rich_text_editor_close.xml | 9 + .../ic_composer_rich_text_editor_edit.xml | 12 + .../drawable/ic_composer_rich_text_save.xml | 16 + .../drawable/ic_composer_strikethrough.xml | 12 + .../res/drawable/ic_composer_underlined.xml | 12 + .../res/drawable/ic_composer_unindent.xml | 12 + app/src/main/res/drawable/ic_quote.xml | 14 + .../res/drawable/ic_rich_composer_add.xml | 15 + .../res/drawable/ic_rich_composer_send.xml | 12 + .../res/layout/composer_rich_text_layout.xml | 206 ++++++ app/src/main/res/layout/view_preview_post.xml | 46 +- .../res/layout/view_rich_text_menu_button.xml | 11 + app/src/main/res/values/attrs.xml | 2 + app/src/main/res/values/dimens.xml | 4 + app/src/main/res/values/strings.xml | 20 + build.gradle | 2 +- 49 files changed, 1998 insertions(+), 48 deletions(-) create mode 100644 app/src/main/java/org/futo/circles/view/Callback.kt create mode 100644 app/src/main/java/org/futo/circles/view/ComposerEditText.kt create mode 100644 app/src/main/java/org/futo/circles/view/DimensionConverter.kt create mode 100644 app/src/main/java/org/futo/circles/view/MessageComposerMode.kt create mode 100644 app/src/main/java/org/futo/circles/view/MessageComposerView.kt create mode 100644 app/src/main/java/org/futo/circles/view/PillDisplayHandler.kt create mode 100644 app/src/main/java/org/futo/circles/view/PillImageSpan.kt create mode 100644 app/src/main/java/org/futo/circles/view/RichTextComposerLayout.kt create mode 100644 app/src/main/java/org/futo/circles/view/RichTextEditorException.kt create mode 100644 app/src/main/java/org/futo/circles/view/SimpleTextWatcher.kt create mode 100644 app/src/main/java/org/futo/circles/view/UriContentListener.kt create mode 100644 app/src/main/res/color/selector_rich_text_menu_icon.xml create mode 100644 app/src/main/res/drawable/bg_code_block.xml create mode 100644 app/src/main/res/drawable/bg_inline_code_multi_line_left.xml create mode 100644 app/src/main/res/drawable/bg_inline_code_multi_line_mid.xml create mode 100644 app/src/main/res/drawable/bg_inline_code_multi_line_right.xml create mode 100644 app/src/main/res/drawable/bg_inline_code_single_line.xml create mode 100644 app/src/main/res/drawable/bg_rich_text_menu_button.xml create mode 100644 app/src/main/res/drawable/ic_composer_bold.xml create mode 100644 app/src/main/res/drawable/ic_composer_bullet_list.xml create mode 100644 app/src/main/res/drawable/ic_composer_code_block.xml create mode 100644 app/src/main/res/drawable/ic_composer_collapse.xml create mode 100644 app/src/main/res/drawable/ic_composer_full_screen.xml create mode 100644 app/src/main/res/drawable/ic_composer_indent.xml create mode 100644 app/src/main/res/drawable/ic_composer_inline_code.xml create mode 100644 app/src/main/res/drawable/ic_composer_italic.xml create mode 100644 app/src/main/res/drawable/ic_composer_link.xml create mode 100644 app/src/main/res/drawable/ic_composer_numbered_list.xml create mode 100644 app/src/main/res/drawable/ic_composer_quote.xml create mode 100644 app/src/main/res/drawable/ic_composer_rich_mic_pressed.xml create mode 100644 app/src/main/res/drawable/ic_composer_rich_text_editor_close.xml create mode 100644 app/src/main/res/drawable/ic_composer_rich_text_editor_edit.xml create mode 100644 app/src/main/res/drawable/ic_composer_rich_text_save.xml create mode 100644 app/src/main/res/drawable/ic_composer_strikethrough.xml create mode 100644 app/src/main/res/drawable/ic_composer_underlined.xml create mode 100644 app/src/main/res/drawable/ic_composer_unindent.xml create mode 100644 app/src/main/res/drawable/ic_quote.xml create mode 100644 app/src/main/res/drawable/ic_rich_composer_add.xml create mode 100644 app/src/main/res/drawable/ic_rich_composer_send.xml create mode 100644 app/src/main/res/layout/composer_rich_text_layout.xml create mode 100644 app/src/main/res/layout/view_rich_text_menu_button.xml diff --git a/app/build.gradle b/app/build.gradle index c7f1c027f..dcb78526d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -22,7 +22,7 @@ android { viewBinding true } - signingConfigs{ + signingConfigs { release { Properties properties = new Properties() if (rootProject.file("signing.properties").exists()) { @@ -77,7 +77,7 @@ dependencies { implementation project(path: ':gallery') //Firebase - gplayImplementation platform('com.google.firebase:firebase-bom:32.5.0') + gplayImplementation platform('com.google.firebase:firebase-bom:32.6.0') gplayImplementation 'com.google.firebase:firebase-crashlytics-ktx' gplayImplementation 'com.google.firebase:firebase-analytics-ktx' gplayImplementation 'com.google.firebase:firebase-messaging-ktx' @@ -94,6 +94,7 @@ dependencies { implementation "io.noties.markwon:linkify:$markwon_version" implementation "io.noties.markwon:ext-strikethrough:$markwon_version" implementation "io.noties.markwon:ext-tasklist:$markwon_version" + implementation "io.element.android:wysiwyg:2.2.2" //Log implementation 'com.jakewharton.timber:timber:5.0.1' diff --git a/app/src/main/java/org/futo/circles/extensions/EditableExtensions.kt b/app/src/main/java/org/futo/circles/extensions/EditableExtensions.kt index bc1f20a2f..2bb56bfad 100644 --- a/app/src/main/java/org/futo/circles/extensions/EditableExtensions.kt +++ b/app/src/main/java/org/futo/circles/extensions/EditableExtensions.kt @@ -1,6 +1,11 @@ package org.futo.circles.extensions import android.text.Editable +import android.text.Spanned +import android.view.View +import android.view.inputmethod.InputMethodManager +import android.widget.EditText +import androidx.core.content.getSystemService import org.futo.circles.feature.timeline.post.markdown.span.TextStyle import org.futo.circles.feature.timeline.post.markdown.span.toSpanClass @@ -15,3 +20,46 @@ fun Editable.getGivenSpansAt( } return spanList } + +fun View.showKeyboard(andRequestFocus: Boolean = false) { + if (andRequestFocus) { + requestFocus() + } + val imm = context?.getSystemService<InputMethodManager>() + imm?.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT) +} + +fun EditText.setTextIfDifferent(newText: CharSequence?): Boolean { + if (!isTextDifferent(newText, text)) { + // Previous text is the same. No op + return false + } + setText(newText) + // Since the text changed we move the cursor to the end of the new text. + // This allows us to fill in text programmatically with a different value, + // but if the user is typing and the view is rebound we won't lose their cursor position. + setSelection(newText?.length ?: 0) + return true +} + +private fun isTextDifferent(str1: CharSequence?, str2: CharSequence?): Boolean { + if (str1 === str2) { + return false + } + if (str1 == null || str2 == null) { + return true + } + val length = str1.length + if (length != str2.length) { + return true + } + if (str1 is Spanned) { + return str1 != str2 + } + for (i in 0 until length) { + if (str1[i] != str2[i]) { + return true + } + } + return false +} diff --git a/app/src/main/java/org/futo/circles/view/Callback.kt b/app/src/main/java/org/futo/circles/view/Callback.kt new file mode 100644 index 000000000..28f3adb08 --- /dev/null +++ b/app/src/main/java/org/futo/circles/view/Callback.kt @@ -0,0 +1,11 @@ +package org.futo.circles.view + + +interface Callback : ComposerEditText.Callback { + fun onCloseRelatedMessage() + fun onSendMessage(text: CharSequence) + fun onAddAttachment() + fun onExpandOrCompactChange() + fun onFullScreenModeChanged() + fun onSetLink(isTextSupported: Boolean, initialLink: String?) +} \ No newline at end of file diff --git a/app/src/main/java/org/futo/circles/view/ComposerEditText.kt b/app/src/main/java/org/futo/circles/view/ComposerEditText.kt new file mode 100644 index 000000000..8dba74e1c --- /dev/null +++ b/app/src/main/java/org/futo/circles/view/ComposerEditText.kt @@ -0,0 +1,123 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.futo.circles.view + +import android.content.Context +import android.net.Uri +import android.os.Build +import android.text.Editable +import android.util.AttributeSet +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputConnection +import androidx.annotation.RequiresApi +import androidx.appcompat.widget.AppCompatEditText +import androidx.core.view.ViewCompat +import androidx.core.view.inputmethod.EditorInfoCompat +import androidx.core.view.inputmethod.InputConnectionCompat +import timber.log.Timber + +class ComposerEditText @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = android.R.attr.editTextStyle +) : AppCompatEditText(context, attrs, defStyleAttr) { + + interface Callback { + fun onRichContentSelected(contentUri: Uri): Boolean + fun onTextChanged(text: CharSequence) + } + + var callback: Callback? = null + + override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection? { + var ic = super.onCreateInputConnection(editorInfo) ?: return null + val mimeTypes = ViewCompat.getOnReceiveContentMimeTypes(this) ?: arrayOf("image/*") + + EditorInfoCompat.setContentMimeTypes(editorInfo, mimeTypes) + ic = InputConnectionCompat.createWrapper(this, ic, editorInfo) + + ViewCompat.setOnReceiveContentListener( + this, + mimeTypes, + UriContentListener { callback?.onRichContentSelected(it) } + ) + + return ic + } + + /** Set whether the keyboard should disable personalized learning. */ + @RequiresApi(Build.VERSION_CODES.O) + fun setUseIncognitoKeyboard(useIncognitoKeyboard: Boolean) { + imeOptions = if (useIncognitoKeyboard) { + imeOptions or EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING + } else { + imeOptions and EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING.inv() + } + } + + /** Set whether enter should send the message or add a new line. */ + fun setSendMessageWithEnter(sendMessageWithEnter: Boolean) { + if (sendMessageWithEnter) { + inputType = inputType and EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE.inv() + imeOptions = imeOptions or EditorInfo.IME_ACTION_SEND + } else { + inputType = inputType or EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE + imeOptions = imeOptions and EditorInfo.IME_ACTION_SEND.inv() + } + } + + init { + addTextChangedListener( + object : SimpleTextWatcher() { + var spanToRemove: PillImageSpan? = null + + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { + Timber.v("Pills: beforeTextChanged: start:$start count:$count after:$after") + + if (count > after) { + // A char has been deleted + val deleteCharPosition = start + count + Timber.v("Pills: beforeTextChanged: deleted char at $deleteCharPosition") + + // Get the first span at this position + spanToRemove = editableText.getSpans(deleteCharPosition, deleteCharPosition, PillImageSpan::class.java) + .ooi { Timber.v("Pills: beforeTextChanged: found ${it.size} span(s)") } + .firstOrNull() + } + } + + override fun afterTextChanged(s: Editable) { + if (spanToRemove != null) { + val start = editableText.getSpanStart(spanToRemove) + val end = editableText.getSpanEnd(spanToRemove) + Timber.v("Pills: afterTextChanged Removing the span start:$start end:$end") + // Must be done before text replacement + editableText.removeSpan(spanToRemove) + if (start != -1 && end != -1) { + editableText.replace(start, end, "") + } + spanToRemove = null + } + callback?.onTextChanged(s.toString()) + } + } + ) + } +} + +inline fun <T> T.ooi(block: (T) -> Unit): T = also(block) diff --git a/app/src/main/java/org/futo/circles/view/DimensionConverter.kt b/app/src/main/java/org/futo/circles/view/DimensionConverter.kt new file mode 100644 index 000000000..2225c471d --- /dev/null +++ b/app/src/main/java/org/futo/circles/view/DimensionConverter.kt @@ -0,0 +1,46 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.futo.circles.view + +import android.content.res.Resources +import android.util.TypedValue +import androidx.annotation.Px +import javax.inject.Inject + +class DimensionConverter @Inject constructor(val resources: Resources) { + + @Px + fun dpToPx(dp: Int): Int { + return TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + dp.toFloat(), + resources.displayMetrics + ).toInt() + } + + @Px + fun spToPx(sp: Int): Int { + return TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_SP, + sp.toFloat(), + resources.displayMetrics + ).toInt() + } + + fun pxToDp(@Px px: Int): Int { + return (px.toFloat() / resources.displayMetrics.density).toInt() + } +} diff --git a/app/src/main/java/org/futo/circles/view/MessageComposerMode.kt b/app/src/main/java/org/futo/circles/view/MessageComposerMode.kt new file mode 100644 index 000000000..c9ebbf983 --- /dev/null +++ b/app/src/main/java/org/futo/circles/view/MessageComposerMode.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.futo.circles.view + + +import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent + +sealed interface MessageComposerMode { + data class Normal(val content: CharSequence?) : MessageComposerMode + + sealed class Special(open val event: TimelineEvent, open val defaultContent: CharSequence) : MessageComposerMode + data class Edit(override val event: TimelineEvent, override val defaultContent: CharSequence) : Special(event, defaultContent) + data class Quote(override val event: TimelineEvent, override val defaultContent: CharSequence) : Special(event, defaultContent) + data class Reply(override val event: TimelineEvent, override val defaultContent: CharSequence) : Special(event, defaultContent) +} diff --git a/app/src/main/java/org/futo/circles/view/MessageComposerView.kt b/app/src/main/java/org/futo/circles/view/MessageComposerView.kt new file mode 100644 index 000000000..e81441dfc --- /dev/null +++ b/app/src/main/java/org/futo/circles/view/MessageComposerView.kt @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.futo.circles.view + +import android.text.Editable +import android.widget.EditText +import android.widget.ImageButton + +interface MessageComposerView { + + companion object { + const val MAX_LINES_WHEN_COLLAPSED = 10 + } + + val text: Editable? + val formattedText: String? + val editText: EditText + val emojiButton: ImageButton? + val sendButton: ImageButton + val attachmentButton: ImageButton + + var callback: Callback? + + fun setTextIfDifferent(text: CharSequence?): Boolean + fun renderComposerMode(mode: MessageComposerMode) +} + diff --git a/app/src/main/java/org/futo/circles/view/PillDisplayHandler.kt b/app/src/main/java/org/futo/circles/view/PillDisplayHandler.kt new file mode 100644 index 000000000..b586e28ec --- /dev/null +++ b/app/src/main/java/org/futo/circles/view/PillDisplayHandler.kt @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.futo.circles.view + +import android.text.style.ReplacementSpan +import io.element.android.wysiwyg.display.KeywordDisplayHandler +import io.element.android.wysiwyg.display.LinkDisplayHandler +import io.element.android.wysiwyg.display.TextDisplay +import org.matrix.android.sdk.api.session.permalinks.PermalinkData +import org.matrix.android.sdk.api.session.permalinks.PermalinkParser +import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary +import org.matrix.android.sdk.api.session.room.model.RoomSummary +import org.matrix.android.sdk.api.util.MatrixItem +import org.matrix.android.sdk.api.util.toEveryoneInRoomMatrixItem +import org.matrix.android.sdk.api.util.toMatrixItem +import org.matrix.android.sdk.api.util.toRoomAliasMatrixItem + +/** + * A rich text editor [LinkDisplayHandler] and [KeywordDisplayHandler] + * that helps with replacing user and room links with pills. + */ +internal class PillDisplayHandler( + private val roomId: String, + private val getRoom: (roomId: String) -> RoomSummary?, + private val getMember: (userId: String) -> RoomMemberSummary?, + private val replacementSpanFactory: (matrixItem: MatrixItem) -> ReplacementSpan, +) : LinkDisplayHandler, KeywordDisplayHandler { + override fun resolveLinkDisplay(text: String, url: String): TextDisplay { + val matrixItem = when (val permalink = PermalinkParser.parse(url)) { + is PermalinkData.UserLink -> { + val userId = permalink.userId + when (val roomMember = getMember(userId)) { + null -> MatrixItem.UserItem(userId, userId, null) + else -> roomMember.toMatrixItem() + } + } + is PermalinkData.RoomLink -> { + val roomId = permalink.roomIdOrAlias + val room = getRoom(roomId) + when { + room == null -> MatrixItem.RoomItem(roomId, roomId, null) + text == MatrixItem.NOTIFY_EVERYONE -> room.toEveryoneInRoomMatrixItem() + permalink.isRoomAlias -> room.toRoomAliasMatrixItem() + else -> room.toMatrixItem() + } + } + else -> + return TextDisplay.Plain + } + val replacement = replacementSpanFactory.invoke(matrixItem) + return TextDisplay.Custom(customSpan = replacement) + } + + override val keywords: List<String> + get() = listOf(MatrixItem.NOTIFY_EVERYONE) + + override fun resolveKeywordDisplay(text: String): TextDisplay = + when (text) { + MatrixItem.NOTIFY_EVERYONE -> { + val matrixItem = getRoom(roomId)?.toEveryoneInRoomMatrixItem() + ?: MatrixItem.EveryoneInRoomItem(roomId) + TextDisplay.Custom(replacementSpanFactory.invoke(matrixItem)) + } + else -> TextDisplay.Plain + } +} diff --git a/app/src/main/java/org/futo/circles/view/PillImageSpan.kt b/app/src/main/java/org/futo/circles/view/PillImageSpan.kt new file mode 100644 index 000000000..ba4a0f78d --- /dev/null +++ b/app/src/main/java/org/futo/circles/view/PillImageSpan.kt @@ -0,0 +1,141 @@ +package org.futo.circles.view + +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.text.TextUtils +import android.text.style.ReplacementSpan +import android.widget.TextView +import androidx.annotation.UiThread +import androidx.appcompat.widget.ThemeUtils +import androidx.core.content.ContextCompat +import com.google.android.material.chip.ChipDrawable +import org.futo.circles.R +import org.futo.circles.core.glide.GlideRequests +import org.matrix.android.sdk.api.session.room.send.MatrixItemSpan +import org.matrix.android.sdk.api.util.MatrixItem +import java.lang.ref.WeakReference + +/** + * This span is able to replace a text by a [ChipDrawable] + * It's needed to call [bind] method to start requesting avatar, otherwise only the placeholder icon will be displayed if not already cached. + * Implements MatrixItemSpan so that it could be automatically transformed in matrix links and displayed as pills. + */ +class PillImageSpan( + private val glideRequests: GlideRequests, + private val context: Context, + override val matrixItem: MatrixItem +) : ReplacementSpan(), MatrixItemSpan { + + private val pillDrawable = createChipDrawable() + // private val target = PillImageSpanTarget(this) + private var tv: WeakReference<TextView>? = null + + @UiThread + fun bind(textView: TextView) { + tv = WeakReference(textView) + //avatarRenderer.render(glideRequests, matrixItem, target) + } + + // ReplacementSpan ***************************************************************************** + + override fun getSize( + paint: Paint, text: CharSequence, + start: Int, + end: Int, + fm: Paint.FontMetricsInt? + ): Int { + val rect = pillDrawable.bounds + if (fm != null) { + val fmPaint = paint.fontMetricsInt + val fontHeight = fmPaint.bottom - fmPaint.top + val drHeight = rect.bottom - rect.top + val top = drHeight / 2 - fontHeight / 4 + val bottom = drHeight / 2 + fontHeight / 4 + fm.ascent = -bottom + fm.top = -bottom + fm.bottom = top + fm.descent = top + } + return rect.right + } + + override fun draw( + canvas: Canvas, text: CharSequence, + start: Int, + end: Int, + x: Float, + top: Int, + y: Int, + bottom: Int, + paint: Paint + ) { + canvas.save() + val fm = paint.fontMetricsInt + val transY: Int = y + (fm.descent + fm.ascent - pillDrawable.bounds.bottom) / 2 + canvas.save() + canvas.translate(x, transY.toFloat()) + + val rect = Rect() + canvas.getClipBounds(rect) + val maxWidth = rect.right + if (pillDrawable.intrinsicWidth > maxWidth) { + pillDrawable.setBounds(0, 0, maxWidth, pillDrawable.intrinsicHeight) + pillDrawable.ellipsize = TextUtils.TruncateAt.END + } + + pillDrawable.draw(canvas) + canvas.restore() + } + + internal fun updateAvatarDrawable(drawable: Drawable?) { + pillDrawable.chipIcon = drawable + tv?.get()?.invalidate() + } + + // Private methods ***************************************************************************** + + private fun createChipDrawable(): ChipDrawable { + val textPadding = context.resources.getDimension(R.dimen.divider_height) + val icon = when { +// matrixItem is MatrixItem.RoomAliasItem && matrixItem.avatarUrl.isNullOrEmpty() && +// matrixItem.displayName == context.getString(R.string.followed_by_format, matrixItem.id) -> { +// ContextCompat.getDrawable(context, R.drawable.ic_permalink_round) +// } +// matrixItem is MatrixItem.RoomItem && matrixItem.avatarUrl.isNullOrEmpty() && ( +// matrixItem.displayName == context.getString(R.string.pill_message_in_unknown_room) || +// matrixItem.displayName == context.getString(R.string.pill_message_unknown_room_or_space) || +// matrixItem.displayName == context.getString(R.string.pill_message_from_unknown_user) +// ) -> { +// ContextCompat.getDrawable(context, R.drawable.ic_composer_bold) +// } + matrixItem is MatrixItem.UserItem && matrixItem.avatarUrl.isNullOrEmpty() -> { + ContextCompat.getDrawable(context, R.drawable.ic_composer_bold) + } + else -> { + try { + // avatarRenderer.getCachedDrawable(glideRequests, matrixItem) + } catch (exception: Exception) { + // avatarRenderer.getPlaceholderDrawable(matrixItem) + } + } + } + + return ChipDrawable.createFromResource(context, R.xml.bg_chip).apply { + text = "matrixItem.getBestName()" + textEndPadding = textPadding + textStartPadding = textPadding + setChipMinHeightResource(R.dimen.rich_text_composer_corner_radius_expanded) + setChipIconSizeResource(R.dimen.rich_text_composer_corner_radius_expanded) + //chipIcon = icon + if (matrixItem is MatrixItem.EveryoneInRoomItem) { + // setTextColor API does not exist right now for ChipDrawable, use textAppearance + } + setBounds(0, 0, intrinsicWidth, intrinsicHeight) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/futo/circles/view/PreviewPostView.kt b/app/src/main/java/org/futo/circles/view/PreviewPostView.kt index 06264d5c6..238befaa9 100644 --- a/app/src/main/java/org/futo/circles/view/PreviewPostView.kt +++ b/app/src/main/java/org/futo/circles/view/PreviewPostView.kt @@ -59,9 +59,9 @@ class PreviewPostView( ) } setOnClickListener { requestFocusOnText() } - binding.etTextPost.doAfterTextChanged { - listener?.onPostContentAvailable(it?.toString()?.isNotBlank() == true) - } +// binding.etTextPost.doAfterTextChanged { +// listener?.onPostContentAvailable(it?.toString()?.isNotBlank() == true) +// } binding.ivRemoveImage.setOnClickListener { setTextContent() } @@ -74,32 +74,32 @@ class PreviewPostView( roomId: String ) { listener = previewPostListener - binding.etTextPost.setHighlightSelectedSpanListener(onHighlightTextStyle) - binding.etTextPost.initMentionsAutocomplete(roomId) + // binding.etTextPost.setHighlightSelectedSpanListener(onHighlightTextStyle) + //binding.etTextPost.initMentionsAutocomplete(roomId) } fun setText(message: String) { - binding.etTextPost.setText( - MarkdownParser.markwonBuilder(context).toMarkdown(message), - TextView.BufferType.SPANNABLE - ) +// binding.etTextPost.setText( +// MarkdownParser.markwonBuilder(context).toMarkdown(message), +// TextView.BufferType.SPANNABLE +// ) setTextContent() } fun setTextStyle(style: TextStyle, isSelected: Boolean) { - binding.etTextPost.triggerStyle(style, isSelected) + // binding.etTextPost.triggerStyle(style, isSelected) } fun insertEmoji(unicode: String) { - binding.etTextPost.insertText(unicode) + // binding.etTextPost.insertText(unicode) } fun insertMention() { - binding.etTextPost.insertMentionMark() + //binding.etTextPost.insertMentionMark() } fun insertLink(title: String?, link: String) { - binding.etTextPost.addLinkSpan(title, link) + // binding.etTextPost.addLinkSpan(title, link) } fun setMediaFromExistingPost(mediaContent: MediaContent) { @@ -121,8 +121,8 @@ class PreviewPostView( fun setMedia(contentUri: Uri, mediaType: MediaType) { - val caption = binding.etTextPost.text.toString().trim() - postContent = MediaPostContent(caption, contentUri, mediaType) + // val caption = binding.etTextPost.text.toString().trim() + // postContent = MediaPostContent(caption, contentUri, mediaType) updateContentView() loadMediaCover(contentUri, mediaType) val isVideo = mediaType == MediaType.Video @@ -134,26 +134,28 @@ class PreviewPostView( listener?.onPostContentAvailable(true) } - fun getPostContent() = (postContent as? MediaPostContent)?.copy( - caption = binding.etTextPost.getTextWithMarkdown().trim().takeIf { it.isNotEmpty() } - ) ?: TextPostContent(binding.etTextPost.getTextWithMarkdown().trim()) + fun getPostContent() = TextPostContent("binding.etTextPost.getMarkdown().trim()") + +// ?.copy( +// caption = binding.etTextPost.getTextWithMarkdown().trim().takeIf { it.isNotEmpty() } +// ) ?: TextPostContent(binding.etTextPost.getTextWithMarkdown().trim()) private fun updateContentView() { val isTextContent = postContent is TextPostContent || postContent == null binding.lMediaContent.lMedia.setIsVisible(!isTextContent) binding.ivRemoveImage.setIsVisible(!isTextContent && canEditMedia) if (isTextContent) requestFocusOnText() - binding.etTextPost.setPadding( - context.convertDpToPixel(12f).toInt(), - 0, context.convertDpToPixel(12f).toInt(), - if (isTextContent) context.convertDpToPixel(64f).toInt() else 0 - ) +// binding.etTextPost.setPadding( +// context.convertDpToPixel(12f).toInt(), +// 0, context.convertDpToPixel(12f).toInt(), +// if (isTextContent) context.convertDpToPixel(64f).toInt() else 0 +// ) } private fun setTextContent() { postContent = null updateContentView() - listener?.onPostContentAvailable(binding.etTextPost.text.toString().isNotBlank()) + listener?.onPostContentAvailable(false) } private fun loadMediaCover(uri: Uri, mediaType: MediaType) { @@ -184,17 +186,17 @@ class PreviewPostView( } private fun requestFocusOnText() { - binding.etTextPost.post { - requestFocus() - binding.etTextPost.setSelection(binding.etTextPost.text.length) - showKeyboard() - } +// binding.etTextPost.post { +// requestFocus() +// // binding.etTextPost.setSelection(binding.etTextPost.text.length) +// showKeyboard() +// } } private fun showKeyboard() { val inputMethodManager: InputMethodManager = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - inputMethodManager.showSoftInput(binding.etTextPost, 0) + // inputMethodManager.showSoftInput(binding.etTextPost, 0) } private fun getMyUser(): User? { diff --git a/app/src/main/java/org/futo/circles/view/RichTextComposerLayout.kt b/app/src/main/java/org/futo/circles/view/RichTextComposerLayout.kt new file mode 100644 index 000000000..d14371955 --- /dev/null +++ b/app/src/main/java/org/futo/circles/view/RichTextComposerLayout.kt @@ -0,0 +1,675 @@ +package org.futo.circles.view + +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import android.annotation.SuppressLint +import android.content.Context +import android.content.res.ColorStateList +import android.content.res.Configuration +import android.graphics.Color +import android.text.Editable +import android.text.TextWatcher +import android.util.AttributeSet +import android.util.TypedValue +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.widget.EditText +import android.widget.ImageButton +import android.widget.LinearLayout +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.constraintlayout.widget.ConstraintSet +import androidx.core.text.toSpannable +import androidx.core.view.ViewCompat +import androidx.core.view.isGone +import androidx.core.view.isInvisible +import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams +import com.google.android.material.shape.MaterialShapeDrawable +import io.element.android.wysiwyg.EditorEditText +import io.element.android.wysiwyg.display.KeywordDisplayHandler +import io.element.android.wysiwyg.display.LinkDisplayHandler +import io.element.android.wysiwyg.display.TextDisplay +import io.element.android.wysiwyg.utils.RustErrorCollector +import io.element.android.wysiwyg.view.models.InlineFormat +import io.element.android.wysiwyg.view.models.LinkAction +import org.futo.circles.R +import org.futo.circles.databinding.ComposerRichTextLayoutBinding +import org.futo.circles.databinding.ViewRichTextMenuButtonBinding +import org.futo.circles.extensions.setTextIfDifferent +import org.futo.circles.extensions.showKeyboard +import uniffi.wysiwyg_composer.ActionState +import uniffi.wysiwyg_composer.ComposerAction + +internal class RichTextComposerLayout @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : LinearLayout(context, attrs, defStyleAttr), MessageComposerView { + + private val views: ComposerRichTextLayoutBinding + + override var callback: Callback? = null + + // There is no need to persist these values since they're always updated by the parent fragment + private var isFullScreen = false + private var hasRelatedMessage = false + private var composerMode: MessageComposerMode? = null + + var isTextFormattingEnabled = true + set(value) { + if (field == value) return + syncEditTexts() + field = value + updateTextFieldBorder(isFullScreen) + updateEditTextVisibility() + updateFullScreenButtonVisibility() + // If formatting is no longer enabled and it's in full screen, minimise the editor + if (!value && isFullScreen) { + callback?.onFullScreenModeChanged() + } + } + + override val text: Editable? + get() = editText.text + override val formattedText: String? + get() = (editText as? EditorEditText)?.getContentAsMessageHtml() + override val editText: EditText + get() = if (isTextFormattingEnabled) { + views.richTextComposerEditText + } else { + views.plainTextComposerEditText + } + override val emojiButton: ImageButton? + get() = null + override val sendButton: ImageButton + get() = views.sendButton + override val attachmentButton: ImageButton + get() = views.attachmentButton + + val richTextEditText: EditText + get() = + views.richTextComposerEditText + val plainTextEditText: EditText + get() = + views.plainTextComposerEditText + + var pillDisplayHandler: PillDisplayHandler? = null + + // Border of the EditText + private val borderShapeDrawable: MaterialShapeDrawable by lazy { + MaterialShapeDrawable().apply { + val typedData = TypedValue() + val lineColor = context.theme.obtainStyledAttributes( + typedData.data, + intArrayOf(R.attr.vctr_content_quaternary) + ) + .getColor(0, 0) + strokeColor = ColorStateList.valueOf(lineColor) + strokeWidth = 1 * resources.displayMetrics.scaledDensity + fillColor = ColorStateList.valueOf(Color.TRANSPARENT) + val cornerSize = + resources.getDimensionPixelSize(R.dimen.rich_text_composer_corner_radius_single_line) + setCornerSize(cornerSize.toFloat()) + } + } + + private val dimensionConverter = DimensionConverter(resources) + + fun setFullScreen(isFullScreen: Boolean, animated: Boolean) { + if (!animated && views.composerLayout.layoutParams != null) { + views.composerLayout.updateLayoutParams<ViewGroup.LayoutParams> { + height = + if (isFullScreen) ViewGroup.LayoutParams.MATCH_PARENT else ViewGroup.LayoutParams.WRAP_CONTENT + } + } + editText.updateLayoutParams<ViewGroup.LayoutParams> { + height = + if (isFullScreen) ViewGroup.LayoutParams.MATCH_PARENT else ViewGroup.LayoutParams.WRAP_CONTENT + } + + updateTextFieldBorder(isFullScreen) + updateEditTextVisibility() + + updateEditTextFullScreenState(views.richTextComposerEditText, isFullScreen) + updateEditTextFullScreenState(views.plainTextComposerEditText, isFullScreen) + + views.composerFullScreenButton.setImageResource( + if (isFullScreen) R.drawable.ic_composer_collapse else R.drawable.ic_composer_full_screen + ) + + views.bottomSheetHandle.isVisible = isFullScreen + if (isFullScreen) { + editText.showKeyboard(true) + } + this.isFullScreen = isFullScreen + } + + fun notifyIsBeingDragged(percentage: Float) { + // Calculate a new shape for the border according to the position in screen + val isSingleLine = editText.lineCount == 1 + val cornerSize = if (!isSingleLine || hasRelatedMessage) { + resources.getDimensionPixelSize(R.dimen.rich_text_composer_corner_radius_expanded) + .toFloat() + } else { + val multilineCornerSize = + resources.getDimensionPixelSize(R.dimen.rich_text_composer_corner_radius_expanded) + val singleLineCornerSize = + resources.getDimensionPixelSize(R.dimen.rich_text_composer_corner_radius_single_line) + val diff = singleLineCornerSize - multilineCornerSize + multilineCornerSize + diff * (1 - percentage) + } + if (cornerSize != borderShapeDrawable.bottomLeftCornerResolvedSize) { + borderShapeDrawable.setCornerSize(cornerSize) + } + + // Change maxLines while dragging, this should improve the smoothness of animations + val maxLines = if (percentage > 0.25f) { + Int.MAX_VALUE + } else { + MessageComposerView.MAX_LINES_WHEN_COLLAPSED + } + views.richTextComposerEditText.maxLines = maxLines + views.plainTextComposerEditText.maxLines = maxLines + + views.bottomSheetHandle.isVisible = true + } + + init { + inflate(context, R.layout.composer_rich_text_layout, this) + views = ComposerRichTextLayoutBinding.bind(this) + + // Workaround to avoid cut-off text caused by padding in scrolled TextView (there is no clipToPadding). + // In TextView, clipTop = padding, but also clipTop -= shadowRadius. So if we set the shadowRadius to padding, they cancel each other + views.richTextComposerEditText.setShadowLayer( + views.richTextComposerEditText.paddingBottom.toFloat(), + 0f, + 0f, + 0 + ) + views.plainTextComposerEditText.setShadowLayer( + views.richTextComposerEditText.paddingBottom.toFloat(), + 0f, + 0f, + 0 + ) + + renderComposerMode(MessageComposerMode.Normal(null)) + + views.richTextComposerEditText.addTextChangedListener( + TextChangeListener( + { callback?.onTextChanged(it) }, + { updateTextFieldBorder(isFullScreen) }) + ) + views.plainTextComposerEditText.addTextChangedListener( + TextChangeListener( + { callback?.onTextChanged(it) }, + { updateTextFieldBorder(isFullScreen) }) + ) + ViewCompat.setOnReceiveContentListener( + views.richTextComposerEditText, + arrayOf("image/*"), + UriContentListener { callback?.onRichContentSelected(it) } + ) + ViewCompat.setOnReceiveContentListener( + views.plainTextComposerEditText, + arrayOf("image/*"), + UriContentListener { callback?.onRichContentSelected(it) } + ) + + disallowParentInterceptTouchEvent(views.richTextComposerEditText) + disallowParentInterceptTouchEvent(views.plainTextComposerEditText) + + views.composerModeCloseView.setOnClickListener { + callback?.onCloseRelatedMessage() + } + + views.sendButton.setOnClickListener { + val textMessage = text?.toSpannable() ?: "" + callback?.onSendMessage(textMessage) + } + + views.attachmentButton.setOnClickListener { + callback?.onAddAttachment() + } + + views.composerFullScreenButton.apply { + updateFullScreenButtonVisibility() + setOnClickListener { + callback?.onFullScreenModeChanged() + } + } + + views.composerEditTextOuterBorder.background = borderShapeDrawable + + setupRichTextMenu() + views.richTextComposerEditText.linkDisplayHandler = LinkDisplayHandler { text, url -> + pillDisplayHandler?.resolveLinkDisplay(text, url) ?: TextDisplay.Plain + } + views.richTextComposerEditText.keywordDisplayHandler = object : KeywordDisplayHandler { + override val keywords: List<String> + get() = pillDisplayHandler?.keywords.orEmpty() + + override fun resolveKeywordDisplay(text: String): TextDisplay = + pillDisplayHandler?.resolveKeywordDisplay(text) ?: TextDisplay.Plain + } + + updateTextFieldBorder(isFullScreen) + } + + private fun setupRichTextMenu() { + addRichTextMenuItem( + R.drawable.ic_composer_bold, + R.string.rich_text_editor_format_bold, + ComposerAction.BOLD + ) { + views.richTextComposerEditText.toggleInlineFormat(InlineFormat.Bold) + } + addRichTextMenuItem( + R.drawable.ic_composer_italic, + R.string.rich_text_editor_format_italic, + ComposerAction.ITALIC + ) { + views.richTextComposerEditText.toggleInlineFormat(InlineFormat.Italic) + } + addRichTextMenuItem( + R.drawable.ic_composer_underlined, + R.string.rich_text_editor_format_underline, + ComposerAction.UNDERLINE + ) { + views.richTextComposerEditText.toggleInlineFormat(InlineFormat.Underline) + } + addRichTextMenuItem( + R.drawable.ic_composer_strikethrough, + R.string.rich_text_editor_format_strikethrough, + ComposerAction.STRIKE_THROUGH + ) { + views.richTextComposerEditText.toggleInlineFormat(InlineFormat.StrikeThrough) + } + addRichTextMenuItem( + R.drawable.ic_composer_bullet_list, + R.string.rich_text_editor_bullet_list, + ComposerAction.UNORDERED_LIST + ) { + views.richTextComposerEditText.toggleList(ordered = false) + } + addRichTextMenuItem( + R.drawable.ic_composer_numbered_list, + R.string.rich_text_editor_numbered_list, + ComposerAction.ORDERED_LIST + ) { + views.richTextComposerEditText.toggleList(ordered = true) + } + addRichTextMenuItem( + R.drawable.ic_composer_indent, + R.string.rich_text_editor_indent, + ComposerAction.INDENT + ) { + views.richTextComposerEditText.indent() + } + addRichTextMenuItem( + R.drawable.ic_composer_unindent, + R.string.rich_text_editor_unindent, + ComposerAction.UNINDENT + ) { + views.richTextComposerEditText.unindent() + } + addRichTextMenuItem( + R.drawable.ic_composer_quote, + R.string.rich_text_editor_quote, + ComposerAction.QUOTE + ) { + views.richTextComposerEditText.toggleQuote() + } + addRichTextMenuItem( + R.drawable.ic_composer_inline_code, + R.string.rich_text_editor_inline_code, + ComposerAction.INLINE_CODE + ) { + views.richTextComposerEditText.toggleInlineFormat(InlineFormat.InlineCode) + } + addRichTextMenuItem( + R.drawable.ic_composer_code_block, + R.string.rich_text_editor_code_block, + ComposerAction.CODE_BLOCK + ) { + views.richTextComposerEditText.toggleCodeBlock() + } + addRichTextMenuItem( + R.drawable.ic_composer_link, + R.string.rich_text_editor_link, + ComposerAction.LINK + ) { + views.richTextComposerEditText.getLinkAction()?.let { + when (it) { + LinkAction.InsertLink -> callback?.onSetLink( + isTextSupported = true, + initialLink = null + ) + + is LinkAction.SetLink -> callback?.onSetLink( + isTextSupported = false, + initialLink = it.currentUrl + ) + } + } + } + } + + fun setLink(link: String?) = + views.richTextComposerEditText.setLink(link) + + fun insertLink(link: String, text: String) = + views.richTextComposerEditText.insertLink(link, text) + + fun removeLink() = + views.richTextComposerEditText.removeLink() + + // Update the API to insertMention when available + fun insertMention(url: String, displayText: String) = + views.richTextComposerEditText.insertLink(url, displayText) + + @SuppressLint("ClickableViewAccessibility") + private fun disallowParentInterceptTouchEvent(view: View) { + view.setOnTouchListener { v, event -> + if (v.hasFocus()) { + v.parent?.requestDisallowInterceptTouchEvent(true) + val action = event.actionMasked + if (action == MotionEvent.ACTION_SCROLL) { + v.parent?.requestDisallowInterceptTouchEvent(false) + return@setOnTouchListener true + } + } + false + } + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + + views.richTextComposerEditText.actionStatesChangedListener = + EditorEditText.OnActionStatesChangedListener { state -> + for (action in state.keys) { + updateMenuStateFor(action, state) + } + } + updateEditTextVisibility() + } + + fun setOnErrorListener(onError: (e: RichTextEditorException) -> Unit) { + views.richTextComposerEditText.rustErrorCollector = RustErrorCollector { + onError(RichTextEditorException(it)) + } + } + + private fun updateEditTextVisibility() { + views.richTextComposerEditText.isVisible = isTextFormattingEnabled + views.richTextMenuScrollView.isVisible = isTextFormattingEnabled + views.plainTextComposerEditText.isVisible = !isTextFormattingEnabled + + // The layouts for formatted text mode and plain text mode are different, so we need to update the constraints + val dpToPx = { dp: Int -> dimensionConverter.dpToPx(dp) } + ConstraintSet().apply { + clone(views.composerLayoutContent) + clear(R.id.composerEditTextOuterBorder, ConstraintSet.TOP) + clear(R.id.composerEditTextOuterBorder, ConstraintSet.BOTTOM) + clear(R.id.composerEditTextOuterBorder, ConstraintSet.START) + clear(R.id.composerEditTextOuterBorder, ConstraintSet.END) + if (isTextFormattingEnabled) { + connect( + R.id.composerEditTextOuterBorder, + ConstraintSet.TOP, + R.id.composerLayoutContent, + ConstraintSet.TOP, + dpToPx(8) + ) + connect( + R.id.composerEditTextOuterBorder, + ConstraintSet.BOTTOM, + R.id.sendButton, + ConstraintSet.TOP, + 0 + ) + connect( + R.id.composerEditTextOuterBorder, + ConstraintSet.START, + R.id.composerLayoutContent, + ConstraintSet.START, + dpToPx(12) + ) + connect( + R.id.composerEditTextOuterBorder, + ConstraintSet.END, + R.id.composerLayoutContent, + ConstraintSet.END, + dpToPx(12) + ) + } else { + connect( + R.id.composerEditTextOuterBorder, + ConstraintSet.TOP, + R.id.composerLayoutContent, + ConstraintSet.TOP, + dpToPx(8) + ) + connect( + R.id.composerEditTextOuterBorder, + ConstraintSet.BOTTOM, + R.id.composerLayoutContent, + ConstraintSet.BOTTOM, + dpToPx(8) + ) + connect( + R.id.composerEditTextOuterBorder, + ConstraintSet.START, + R.id.attachmentButton, + ConstraintSet.END, + 0 + ) + connect( + R.id.composerEditTextOuterBorder, + ConstraintSet.END, + R.id.sendButton, + ConstraintSet.START, + 0 + ) + } + applyTo(views.composerLayoutContent) + } + } + + private fun updateFullScreenButtonVisibility() { + val isLargeScreenDevice = + resources.configuration.isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_LARGE) + val isLandscape = resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE + // There's no point in having full screen in landscape since there's almost no vertical space + views.composerFullScreenButton.isInvisible = + !isTextFormattingEnabled || (isLandscape && !isLargeScreenDevice) + } + + /** + * Updates the non-active input with the contents of the active input. + */ + private fun syncEditTexts() = + if (isTextFormattingEnabled) { + views.plainTextComposerEditText.setText(views.richTextComposerEditText.getMarkdown()) + } else { + views.richTextComposerEditText.setMarkdown(views.plainTextComposerEditText.text.toString()) + } + + private fun addRichTextMenuItem( + @DrawableRes iconId: Int, + @StringRes description: Int, + action: ComposerAction, + onClick: () -> Unit + ) { + val inflater = LayoutInflater.from(context) + val button = ViewRichTextMenuButtonBinding.inflate(inflater, views.richTextMenu, true) + button.root.tag = action + with(button.root) { + contentDescription = resources.getString(description) + setImageResource(iconId) + setOnClickListener { + onClick() + } + } + } + + private fun updateMenuStateFor( + action: ComposerAction, + menuState: Map<ComposerAction, ActionState> + ) { + val button = findViewWithTag<ImageButton>(action) ?: return + val stateForAction = menuState[action] + button.isEnabled = stateForAction != ActionState.DISABLED + button.isSelected = stateForAction == ActionState.REVERSED + + if (action == ComposerAction.INDENT || action == ComposerAction.UNINDENT) { + val indentationButtonIsVisible = + menuState[ComposerAction.ORDERED_LIST] == ActionState.REVERSED || + menuState[ComposerAction.UNORDERED_LIST] == ActionState.REVERSED + button.isVisible = indentationButtonIsVisible + } + } + + fun estimateCollapsedHeight(): Int { + val editText = this.editText + val originalLines = editText.maxLines + val originalParamsHeight = editText.layoutParams.height + editText.maxLines = MessageComposerView.MAX_LINES_WHEN_COLLAPSED + editText.layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT + measure( + MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + MeasureSpec.UNSPECIFIED, + ) + val result = measuredHeight + editText.layoutParams.height = originalParamsHeight + editText.maxLines = originalLines + return result + } + + private fun updateTextFieldBorder(isFullScreen: Boolean) { + val isMultiline = + editText.editableText.lines().count() > 1 || isFullScreen || hasRelatedMessage + val cornerSize = if (isMultiline) { + resources.getDimensionPixelSize(R.dimen.rich_text_composer_corner_radius_expanded) + } else { + resources.getDimensionPixelSize(R.dimen.rich_text_composer_corner_radius_single_line) + }.toFloat() + borderShapeDrawable.setCornerSize(cornerSize) + } + + private fun replaceFormattedContent(text: CharSequence) { + views.richTextComposerEditText.setHtml(text.toString()) + updateTextFieldBorder(isFullScreen) + } + + override fun setTextIfDifferent(text: CharSequence?): Boolean { + val result = editText.setTextIfDifferent(text) + updateTextFieldBorder(isFullScreen) + return result + } + + private fun updateEditTextFullScreenState(editText: EditText, isFullScreen: Boolean) { + if (isFullScreen) { + editText.maxLines = Int.MAX_VALUE + } else { + editText.maxLines = MessageComposerView.MAX_LINES_WHEN_COLLAPSED + } + } + + override fun renderComposerMode(mode: MessageComposerMode) { + if (mode is MessageComposerMode.Special) { + views.composerModeGroup.isVisible = true + if (isTextFormattingEnabled) { + replaceFormattedContent(mode.defaultContent) + } else { + views.plainTextComposerEditText.setText(mode.defaultContent) + } + hasRelatedMessage = true + editText.showKeyboard(andRequestFocus = true) + } else { + views.composerModeGroup.isGone = true + (mode as? MessageComposerMode.Normal)?.content?.let { text -> + if (isTextFormattingEnabled) { + replaceFormattedContent(text) + } else { + views.plainTextComposerEditText.setText(text) + } + } + hasRelatedMessage = false + } + + updateTextFieldBorder(isFullScreen) + + if (this.composerMode == mode) return + this.composerMode = mode + + views.sendButton.apply { + if (mode is MessageComposerMode.Edit) { + contentDescription = resources.getString(R.string.action_save) + setImageResource(R.drawable.ic_composer_rich_text_save) + } else { + contentDescription = resources.getString(R.string.action_send) + setImageResource(R.drawable.ic_rich_composer_send) + } + } + + when (mode) { + is MessageComposerMode.Edit -> { + views.composerModeTitleView.setText(R.string.editing) + views.composerModeIconView.setImageResource(R.drawable.ic_composer_rich_text_editor_edit) + } + + is MessageComposerMode.Quote -> { + views.composerModeTitleView.setText(R.string.quoting) + views.composerModeIconView.setImageResource(R.drawable.ic_quote) + } + + is MessageComposerMode.Reply -> { + val senderInfo = mode.event.senderInfo + val userName = senderInfo.displayName ?: senderInfo.disambiguatedDisplayName + views.composerModeTitleView.text = + resources.getString(R.string.replying_to, userName) + views.composerModeIconView.setImageResource(R.drawable.ic_reply) + } + + else -> Unit + } + } + + private class TextChangeListener( + private val onTextChanged: (s: Editable) -> Unit, + private val onExpandedChanged: (isExpanded: Boolean) -> Unit, + ) : TextWatcher { + private var previousTextWasExpanded = false + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + override fun afterTextChanged(s: Editable) { + onTextChanged.invoke(s) + + val isExpanded = s.lines().count() > 1 + if (previousTextWasExpanded != isExpanded) { + onExpandedChanged(isExpanded) + } + previousTextWasExpanded = isExpanded + } + } +} diff --git a/app/src/main/java/org/futo/circles/view/RichTextEditorException.kt b/app/src/main/java/org/futo/circles/view/RichTextEditorException.kt new file mode 100644 index 000000000..fdc51ac1c --- /dev/null +++ b/app/src/main/java/org/futo/circles/view/RichTextEditorException.kt @@ -0,0 +1,5 @@ +package org.futo.circles.view + +internal class RichTextEditorException( + cause: Throwable, +) : Exception(cause) \ No newline at end of file diff --git a/app/src/main/java/org/futo/circles/view/SimpleTextWatcher.kt b/app/src/main/java/org/futo/circles/view/SimpleTextWatcher.kt new file mode 100644 index 000000000..d471738bd --- /dev/null +++ b/app/src/main/java/org/futo/circles/view/SimpleTextWatcher.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2019 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.futo.circles.view + +import android.text.Editable +import android.text.TextWatcher + +/** + * TextWatcher with default no op implementation. + */ +open class SimpleTextWatcher : TextWatcher { + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) { + // No op + } + + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { + // No op + } + + override fun afterTextChanged(s: Editable) { + // No op + } +} diff --git a/app/src/main/java/org/futo/circles/view/UriContentListener.kt b/app/src/main/java/org/futo/circles/view/UriContentListener.kt new file mode 100644 index 000000000..3cc04857f --- /dev/null +++ b/app/src/main/java/org/futo/circles/view/UriContentListener.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.futo.circles.view + +import android.content.ClipData +import android.net.Uri +import android.view.View +import androidx.core.view.ContentInfoCompat +import androidx.core.view.OnReceiveContentListener + +class UriContentListener( + private val onContent: (uri: Uri) -> Unit +) : OnReceiveContentListener { + override fun onReceiveContent(view: View, payload: ContentInfoCompat): ContentInfoCompat? { + val split = payload.partition { item -> item.uri != null } + val uriContent = split.first + val remaining = split.second + + if (uriContent != null) { + val clip: ClipData = uriContent.clip + for (i in 0 until clip.itemCount) { + val uri = clip.getItemAt(i).uri + // ... app-specific logic to handle the URI ... + onContent(uri) + } + } + // Return anything that we didn't handle ourselves. This preserves the default platform + // behavior for text and anything else for which we are not implementing custom handling. + return remaining + } +} diff --git a/app/src/main/res/color/selector_rich_text_menu_icon.xml b/app/src/main/res/color/selector_rich_text_menu_icon.xml new file mode 100644 index 000000000..cc4a66618 --- /dev/null +++ b/app/src/main/res/color/selector_rich_text_menu_icon.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_enabled="false" android:color="@color/red" /> + <item android:state_pressed="true" android:color="?attr/colorSecondary" /> + <item android:state_hovered="true" android:color="?attr/colorSecondary" /> + <item android:state_selected="true" android:color="?attr/colorSecondary" /> + <item android:color="@color/blue" /> +</selector> diff --git a/app/src/main/res/drawable/bg_code_block.xml b/app/src/main/res/drawable/bg_code_block.xml new file mode 100644 index 000000000..e171c7d90 --- /dev/null +++ b/app/src/main/res/drawable/bg_code_block.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2018 The Android Open Source Project + ~ Modifications Copyright 2022 New Vector Ltd + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <solid android:color="@color/blue"/> + <stroke android:width="@dimen/code_block_border_width" android:color="@color/blue"/> + <corners android:radius="@dimen/code_block_border_radius"/> +</shape> diff --git a/app/src/main/res/drawable/bg_inline_code_multi_line_left.xml b/app/src/main/res/drawable/bg_inline_code_multi_line_left.xml new file mode 100644 index 000000000..0c942b981 --- /dev/null +++ b/app/src/main/res/drawable/bg_inline_code_multi_line_left.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2018 The Android Open Source Project + ~ Modifications Copyright 2022 New Vector Ltd + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <solid android:color="@color/blue"/> + <stroke android:width="@dimen/inline_code_border_width" android:color="@color/blue"/> + <corners android:topLeftRadius="@dimen/inline_code_border_radius" + android:bottomLeftRadius="@dimen/inline_code_border_radius"/> +</shape> diff --git a/app/src/main/res/drawable/bg_inline_code_multi_line_mid.xml b/app/src/main/res/drawable/bg_inline_code_multi_line_mid.xml new file mode 100644 index 000000000..0d906c99c --- /dev/null +++ b/app/src/main/res/drawable/bg_inline_code_multi_line_mid.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2018 The Android Open Source Project + ~ Modifications Copyright 2022 New Vector Ltd + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <solid android:color="@color/blue"/> + <stroke android:width="@dimen/inline_code_border_width" android:color="@color/blue"/> +</shape> diff --git a/app/src/main/res/drawable/bg_inline_code_multi_line_right.xml b/app/src/main/res/drawable/bg_inline_code_multi_line_right.xml new file mode 100644 index 000000000..495dd1dc4 --- /dev/null +++ b/app/src/main/res/drawable/bg_inline_code_multi_line_right.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2018 The Android Open Source Project + ~ Modifications Copyright 2022 New Vector Ltd + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <solid android:color="@color/blue"/> + <stroke android:width="@dimen/inline_code_border_width" android:color="@color/blue"/> + <corners android:topRightRadius="@dimen/inline_code_border_radius" + android:bottomRightRadius="@dimen/inline_code_border_radius"/> +</shape> diff --git a/app/src/main/res/drawable/bg_inline_code_single_line.xml b/app/src/main/res/drawable/bg_inline_code_single_line.xml new file mode 100644 index 000000000..f2f3c587e --- /dev/null +++ b/app/src/main/res/drawable/bg_inline_code_single_line.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2018 The Android Open Source Project + ~ Modifications Copyright 2022 New Vector Ltd + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <solid android:color="@color/blue"/> + <stroke android:width="@dimen/inline_code_border_width" android:color="@color/blue"/> + <corners android:radius="@dimen/inline_code_border_radius"/> +</shape> diff --git a/app/src/main/res/drawable/bg_rich_text_menu_button.xml b/app/src/main/res/drawable/bg_rich_text_menu_button.xml new file mode 100644 index 000000000..7b436920f --- /dev/null +++ b/app/src/main/res/drawable/bg_rich_text_menu_button.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="utf-8"?> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_hovered="true"> + <shape android:shape="rectangle"> + <corners android:radius="8dp" /> + <solid android:color="@color/blue" /> + </shape> + </item> + <item android:state_selected="true"> + <shape android:shape="rectangle"> + <corners android:radius="8dp" /> + <solid android:color="@color/blue" /> + </shape> + </item> + <item> + <ripple android:color="@color/blue" /> + </item> +</selector> diff --git a/app/src/main/res/drawable/ic_composer_bold.xml b/app/src/main/res/drawable/ic_composer_bold.xml new file mode 100644 index 000000000..d2e26cf1e --- /dev/null +++ b/app/src/main/res/drawable/ic_composer_bold.xml @@ -0,0 +1,10 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="44dp" + android:height="44dp" + android:viewportWidth="44" + android:viewportHeight="44"> + <path + android:fillColor="@color/blue" + android:fillType="evenOdd" + android:pathData="M16,14.5C16,13.672 16.672,13 17.5,13H22.288C25.139,13 27.25,15.466 27.25,18.25C27.25,19.38 26.902,20.458 26.298,21.34C27.765,22.268 28.75,23.882 28.75,25.75C28.75,28.689 26.311,31 23.393,31H17.5C16.672,31 16,30.328 16,29.5V14.5ZM19,16V20.5H22.288C23.261,20.5 24.25,19.608 24.25,18.25C24.25,16.892 23.261,16 22.288,16H19ZM19,23.5V28H23.393C24.735,28 25.75,26.953 25.75,25.75C25.75,24.547 24.735,23.5 23.393,23.5H19Z" /> +</vector> diff --git a/app/src/main/res/drawable/ic_composer_bullet_list.xml b/app/src/main/res/drawable/ic_composer_bullet_list.xml new file mode 100644 index 000000000..d372209ce --- /dev/null +++ b/app/src/main/res/drawable/ic_composer_bullet_list.xml @@ -0,0 +1,12 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="44dp" + android:height="44dp" + android:viewportWidth="44" + android:viewportHeight="44"> + <group> + <clip-path android:pathData="M10,10h24v24h-24z" /> + <path + android:fillColor="@color/blue" + android:pathData="M14,20.5C13.17,20.5 12.5,21.17 12.5,22C12.5,22.83 13.17,23.5 14,23.5C14.83,23.5 15.5,22.83 15.5,22C15.5,21.17 14.83,20.5 14,20.5ZM14,14.5C13.17,14.5 12.5,15.17 12.5,16C12.5,16.83 13.17,17.5 14,17.5C14.83,17.5 15.5,16.83 15.5,16C15.5,15.17 14.83,14.5 14,14.5ZM14,26.5C13.17,26.5 12.5,27.18 12.5,28C12.5,28.82 13.18,29.5 14,29.5C14.82,29.5 15.5,28.82 15.5,28C15.5,27.18 14.83,26.5 14,26.5ZM18,29H30C30.55,29 31,28.55 31,28C31,27.45 30.55,27 30,27H18C17.45,27 17,27.45 17,28C17,28.55 17.45,29 18,29ZM18,23H30C30.55,23 31,22.55 31,22C31,21.45 30.55,21 30,21H18C17.45,21 17,21.45 17,22C17,22.55 17.45,23 18,23ZM17,16C17,16.55 17.45,17 18,17H30C30.55,17 31,16.55 31,16C31,15.45 30.55,15 30,15H18C17.45,15 17,15.45 17,16Z" /> + </group> +</vector> diff --git a/app/src/main/res/drawable/ic_composer_code_block.xml b/app/src/main/res/drawable/ic_composer_code_block.xml new file mode 100644 index 000000000..25155167b --- /dev/null +++ b/app/src/main/res/drawable/ic_composer_code_block.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="44dp" + android:height="44dp" + android:viewportWidth="44" + android:viewportHeight="44"> + <path + android:fillColor="@color/blue" + android:pathData="m18,22c0.7,-0.72 1.4,-1.4 2.1,-2.1 0.68,-0.98 -0.93,-1.9 -1.5,-0.96 -0.87,0.88 -1.8,1.7 -2.6,2.6 -0.45,0.67 0.27,1.2 0.7,1.6 0.75,0.74 1.5,1.5 2.2,2.2 0.98,0.68 1.9,-0.93 0.96,-1.5l-1.9,-1.9zM26.6,22c-0.71,0.72 -1.5,1.4 -2.1,2.2 -0.68,0.98 0.93,1.9 1.5,0.96 0.88,-0.89 1.8,-1.8 2.6,-2.7 0.45,-0.67 -0.27,-1.2 -0.7,-1.6 -0.75,-0.74 -1.5,-1.5 -2.2,-2.2 -0.99,-0.66 -2,0.94 -0.96,1.5l1.9,1.9zM13.6,32c-1.1,0.021 -1.9,-1 -1.7,-2.1 0.005,-5.7 -0.011,-11 0.008,-17 0.088,-1 1.1,-1.7 2.1,-1.5 5.6,0.005 11,-0.011 17,0.008 1,0.088 1.7,1.1 1.5,2.1 -0.005,5.6 0.011,11 -0.008,17 -0.088,1 -1.1,1.7 -2.1,1.5h-17zM13.6,30.3h17v-17h-17v17z" /> +</vector> diff --git a/app/src/main/res/drawable/ic_composer_collapse.xml b/app/src/main/res/drawable/ic_composer_collapse.xml new file mode 100644 index 000000000..d123caac5 --- /dev/null +++ b/app/src/main/res/drawable/ic_composer_collapse.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="20dp" + android:height="20dp" + android:viewportWidth="20" + android:viewportHeight="20"> + <path + android:fillColor="?vctr_content_quaternary" + android:pathData="M10.708,10Q10.438,10 10.219,9.781Q10,9.562 10,9.292V4.542Q10,4.354 10.146,4.219Q10.292,4.083 10.458,4.083Q10.646,4.083 10.781,4.219Q10.917,4.354 10.917,4.542V8.438L16.375,3Q16.5,2.854 16.688,2.854Q16.875,2.854 17,3Q17.146,3.125 17.146,3.312Q17.146,3.5 17,3.625L11.562,9.083H15.458Q15.646,9.083 15.781,9.229Q15.917,9.375 15.917,9.542Q15.917,9.729 15.781,9.865Q15.646,10 15.458,10ZM3,17Q2.854,16.875 2.854,16.688Q2.854,16.5 3,16.375L8.438,10.917H4.542Q4.354,10.917 4.219,10.771Q4.083,10.625 4.083,10.458Q4.083,10.271 4.219,10.135Q4.354,10 4.542,10H9.292Q9.562,10 9.781,10.219Q10,10.438 10,10.708V15.458Q10,15.646 9.854,15.781Q9.708,15.917 9.542,15.917Q9.354,15.917 9.219,15.781Q9.083,15.646 9.083,15.458V11.562L3.625,17Q3.5,17.146 3.312,17.146Q3.125,17.146 3,17Z" /> +</vector> diff --git a/app/src/main/res/drawable/ic_composer_full_screen.xml b/app/src/main/res/drawable/ic_composer_full_screen.xml new file mode 100644 index 000000000..6c7d7d673 --- /dev/null +++ b/app/src/main/res/drawable/ic_composer_full_screen.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="20dp" + android:height="20dp" + android:viewportWidth="20" + android:viewportHeight="20"> + <path + android:fillColor="?vctr_content_quaternary" + android:pathData="M3.625,17.083Q3.354,17.083 3.135,16.865Q2.917,16.646 2.917,16.375V11.625Q2.917,11.438 3.062,11.302Q3.208,11.167 3.375,11.167Q3.562,11.167 3.698,11.302Q3.833,11.438 3.833,11.625V15.5L15.5,3.833H11.625Q11.438,3.833 11.302,3.688Q11.167,3.542 11.167,3.375Q11.167,3.188 11.302,3.052Q11.438,2.917 11.625,2.917H16.375Q16.646,2.917 16.865,3.135Q17.083,3.354 17.083,3.625V8.375Q17.083,8.562 16.938,8.698Q16.792,8.833 16.625,8.833Q16.438,8.833 16.302,8.698Q16.167,8.562 16.167,8.375V4.5L4.5,16.167H8.375Q8.562,16.167 8.698,16.312Q8.833,16.458 8.833,16.625Q8.833,16.812 8.698,16.948Q8.562,17.083 8.375,17.083Z" /> +</vector> diff --git a/app/src/main/res/drawable/ic_composer_indent.xml b/app/src/main/res/drawable/ic_composer_indent.xml new file mode 100644 index 000000000..775d434e3 --- /dev/null +++ b/app/src/main/res/drawable/ic_composer_indent.xml @@ -0,0 +1,12 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="44dp" + android:height="44dp" + android:viewportWidth="44" + android:viewportHeight="44"> + <group> + <clip-path android:pathData="M10,10h24v24h-24z" /> + <path + android:fillColor="@color/blue" + android:pathData="M14,31H30C30.55,31 31,30.55 31,30C31,29.45 30.55,29 30,29H14C13.45,29 13,29.45 13,30C13,30.55 13.45,31 14,31ZM13,19.21V24.8C13,25.25 13.54,25.47 13.85,25.15L16.64,22.36C16.84,22.16 16.84,21.85 16.64,21.65L13.85,18.85C13.54,18.54 13,18.76 13,19.21ZM22,27H30C30.55,27 31,26.55 31,26C31,25.45 30.55,25 30,25H22C21.45,25 21,25.45 21,26C21,26.55 21.45,27 22,27ZM13,14C13,14.55 13.45,15 14,15H30C30.55,15 31,14.55 31,14C31,13.45 30.55,13 30,13H14C13.45,13 13,13.45 13,14ZM22,19H30C30.55,19 31,18.55 31,18C31,17.45 30.55,17 30,17H22C21.45,17 21,17.45 21,18C21,18.55 21.45,19 22,19ZM22,23H30C30.55,23 31,22.55 31,22C31,21.45 30.55,21 30,21H22C21.45,21 21,21.45 21,22C21,22.55 21.45,23 22,23Z" /> + </group> +</vector> diff --git a/app/src/main/res/drawable/ic_composer_inline_code.xml b/app/src/main/res/drawable/ic_composer_inline_code.xml new file mode 100644 index 000000000..bc054e639 --- /dev/null +++ b/app/src/main/res/drawable/ic_composer_inline_code.xml @@ -0,0 +1,15 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="44dp" + android:height="44dp" + android:viewportWidth="44" + android:viewportHeight="44"> + <path + android:fillColor="@color/blue" + android:pathData="M24.958,15.621C25.117,15.092 24.816,14.534 24.287,14.375C23.758,14.217 23.201,14.517 23.042,15.046L19.042,28.379C18.883,28.908 19.184,29.466 19.713,29.624C20.242,29.783 20.799,29.483 20.958,28.954L24.958,15.621Z" /> + <path + android:fillColor="@color/blue" + android:pathData="M15.974,17.232C15.549,16.878 14.919,16.936 14.565,17.36L11.232,21.36C10.923,21.731 10.923,22.269 11.232,22.64L14.565,26.64C14.919,27.065 15.549,27.122 15.974,26.768C16.398,26.415 16.455,25.784 16.102,25.36L13.302,22L16.102,18.64C16.455,18.216 16.398,17.585 15.974,17.232Z" /> + <path + android:fillColor="@color/blue" + android:pathData="M28.027,17.232C28.451,16.878 29.081,16.936 29.435,17.36L32.768,21.36C33.077,21.731 33.077,22.269 32.768,22.64L29.435,26.64C29.081,27.065 28.451,27.122 28.027,26.768C27.602,26.415 27.545,25.784 27.898,25.36L30.698,22L27.898,18.64C27.545,18.216 27.602,17.585 28.027,17.232Z" /> +</vector> diff --git a/app/src/main/res/drawable/ic_composer_italic.xml b/app/src/main/res/drawable/ic_composer_italic.xml new file mode 100644 index 000000000..15c40a206 --- /dev/null +++ b/app/src/main/res/drawable/ic_composer_italic.xml @@ -0,0 +1,10 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="44dp" + android:height="44dp" + android:viewportWidth="44" + android:viewportHeight="44"> + <path + android:pathData="M22.619,14.999L19.747,29.005H17.2C16.758,29.005 16.4,29.363 16.4,29.805C16.4,30.247 16.758,30.605 17.2,30.605H20.389C20.397,30.605 20.405,30.605 20.412,30.605H23.6C24.042,30.605 24.4,30.247 24.4,29.805C24.4,29.363 24.042,29.005 23.6,29.005H21.381L24.253,14.999H26.8C27.242,14.999 27.6,14.64 27.6,14.199C27.6,13.757 27.242,13.399 26.8,13.399H23.615C23.604,13.398 23.594,13.398 23.583,13.399H20.4C19.958,13.399 19.6,13.757 19.6,14.199C19.6,14.64 19.958,14.999 20.4,14.999H22.619Z" + android:fillColor="@color/blue" + android:fillType="evenOdd" /> +</vector> diff --git a/app/src/main/res/drawable/ic_composer_link.xml b/app/src/main/res/drawable/ic_composer_link.xml new file mode 100644 index 000000000..be76286af --- /dev/null +++ b/app/src/main/res/drawable/ic_composer_link.xml @@ -0,0 +1,12 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="44dp" + android:height="44dp" + android:viewportWidth="44" + android:viewportHeight="44"> + <path + android:fillColor="#00000000" + android:pathData="M22.566,16.151L23.101,15.616C24.577,14.14 26.956,14.126 28.415,15.585C29.874,17.044 29.86,19.423 28.383,20.899L25.844,23.438C24.368,24.915 21.989,24.929 20.53,23.47M21.434,27.849L20.899,28.383C19.423,29.86 17.044,29.874 15.585,28.415C14.126,26.956 14.14,24.577 15.616,23.101L18.156,20.562C19.632,19.086 22.011,19.071 23.47,20.53" + android:strokeWidth="1.5" + android:strokeColor="@color/blue" + android:strokeLineCap="round" /> +</vector> diff --git a/app/src/main/res/drawable/ic_composer_numbered_list.xml b/app/src/main/res/drawable/ic_composer_numbered_list.xml new file mode 100644 index 000000000..5690e2de9 --- /dev/null +++ b/app/src/main/res/drawable/ic_composer_numbered_list.xml @@ -0,0 +1,24 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="44dp" + android:height="44dp" + android:viewportWidth="44" + android:viewportHeight="44"> + <path + android:pathData="m14.5,20h-2c-0.28,0 -0.5,0.22 -0.5,0.5 0,0.28 0.22,0.5 0.5,0.5h1.3l-1.68,1.96C12.04,23.05 12,23.17 12,23.28v0.22c0,0.28 0.22,0.5 0.5,0.5h2C14.78,24 15,23.78 15,23.5 15,23.22 14.78,23 14.5,23h-1.3l1.68,-1.96C14.96,20.95 15,20.83 15,20.72V20.5C15,20.22 14.78,20 14.5,20Z" + android:fillColor="@color/blue" /> + <path + android:pathData="M12.5,15H13v2.5c0,0.28 0.22,0.5 0.5,0.5 0.28,0 0.5,-0.22 0.5,-0.5v-3C14,14.22 13.78,14 13.5,14h-1c-0.28,0 -0.5,0.22 -0.5,0.5 0,0.28 0.22,0.5 0.5,0.5z" + android:fillColor="@color/blue" /> + <path + android:pathData="m14.5,26h-2c-0.28,0 -0.5,0.22 -0.5,0.5 0,0.28 0.22,0.5 0.5,0.5H14v0.5h-0.5c-0.28,0 -0.5,0.22 -0.5,0.5 0,0.28 0.22,0.5 0.5,0.5H14V29h-1.5c-0.28,0 -0.5,0.22 -0.5,0.5 0,0.28 0.22,0.5 0.5,0.5h2c0.28,0 0.5,-0.22 0.5,-0.5v-3C15,26.22 14.78,26 14.5,26Z" + android:fillColor="@color/blue" /> + <path + android:pathData="M30,21H18c-0.55,0 -1,0.45 -1,1 0,0.55 0.45,1 1,1h12c0.55,0 1,-0.45 1,-1 0,-0.55 -0.45,-1 -1,-1z" + android:fillColor="@color/blue" /> + <path + android:pathData="M30,27H18c-0.55,0 -1,0.45 -1,1 0,0.55 0.45,1 1,1h12c0.55,0 1,-0.45 1,-1 0,-0.55 -0.45,-1 -1,-1z" + android:fillColor="@color/blue" /> + <path + android:pathData="m18,17h12c0.55,0 1,-0.45 1,-1 0,-0.55 -0.45,-1 -1,-1H18c-0.55,0 -1,0.45 -1,1 0,0.55 0.45,1 1,1z" + android:fillColor="@color/blue" /> +</vector> diff --git a/app/src/main/res/drawable/ic_composer_quote.xml b/app/src/main/res/drawable/ic_composer_quote.xml new file mode 100644 index 000000000..78603bf36 --- /dev/null +++ b/app/src/main/res/drawable/ic_composer_quote.xml @@ -0,0 +1,18 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="44dp" + android:viewportWidth="36" + android:viewportHeight="44"> + <path + android:pathData="M14.719,14.34C14.813,13.698 14.353,13.104 13.691,13.012C13.028,12.92 12.415,13.366 12.32,14.008L11.512,19.486C11.418,20.128 11.878,20.723 12.54,20.814C13.203,20.906 13.816,20.46 13.911,19.818L14.719,14.34Z" + android:fillColor="@color/blue" /> + <path + android:pathData="M26.834,24.514C26.928,23.872 26.468,23.277 25.806,23.186C25.143,23.094 24.53,23.54 24.435,24.182L23.628,29.66C23.533,30.302 23.993,30.896 24.656,30.988C25.318,31.08 25.932,30.634 26.026,29.992L26.834,24.514Z" + android:fillColor="@color/blue" /> + <path + android:pathData="M19.318,13.009C19.983,13.086 20.456,13.671 20.376,14.315L20.354,14.49C20.34,14.602 20.319,14.763 20.293,14.961C20.242,15.358 20.17,15.902 20.088,16.496C19.927,17.667 19.72,19.075 19.553,19.882C19.422,20.518 18.784,20.931 18.128,20.803C17.472,20.676 17.046,20.058 17.177,19.422C17.326,18.701 17.523,17.37 17.687,16.185C17.767,15.599 17.838,15.061 17.889,14.669C17.915,14.473 17.935,14.314 17.949,14.204L17.97,14.034C18.051,13.39 18.654,12.931 19.318,13.009Z" + android:fillColor="@color/blue" /> + <path + android:pathData="M32.488,24.514C32.582,23.872 32.122,23.277 31.46,23.186C30.797,23.094 30.184,23.54 30.089,24.182L29.281,29.66C29.187,30.302 29.647,30.896 30.309,30.988C30.972,31.08 31.585,30.634 31.68,29.992L32.488,24.514Z" + android:fillColor="@color/blue" /> +</vector> diff --git a/app/src/main/res/drawable/ic_composer_rich_mic_pressed.xml b/app/src/main/res/drawable/ic_composer_rich_mic_pressed.xml new file mode 100644 index 000000000..e9dbe610e --- /dev/null +++ b/app/src/main/res/drawable/ic_composer_rich_mic_pressed.xml @@ -0,0 +1,17 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="52dp" + android:height="52dp" + android:viewportWidth="52" + android:viewportHeight="52"> + <path + android:pathData="M26.173,26.169m-22.763,0a22.763,22.763 0,1 1,45.526 0a22.763,22.763 0,1 1,-45.526 0" + android:fillColor="#0DBD8B"/> + <path + android:pathData="M26,26m-26,0a26,26 0,1 1,52 0a26,26 0,1 1,-52 0" + android:strokeAlpha="0.2" + android:fillColor="#0DBD8B" + android:fillAlpha="0.2"/> + <path + android:pathData="M26,29.5C27.937,29.5 29.488,27.937 29.488,26L29.5,19C29.5,17.063 27.937,15.5 26,15.5C24.063,15.5 22.5,17.063 22.5,19V26C22.5,27.937 24.063,29.5 26,29.5ZM33.093,26C32.603,26 32.195,26.35 32.125,26.828C31.693,29.873 28.952,31.95 26,31.95C23.048,31.95 20.307,29.885 19.875,26.828C19.805,26.35 19.385,26 18.907,26C18.3,26 17.833,26.537 17.915,27.132C18.452,30.597 21.368,33.315 24.833,33.84V36.5C24.833,37.142 25.358,37.667 26,37.667C26.642,37.667 27.167,37.142 27.167,36.5V33.84C30.62,33.338 33.548,30.597 34.085,27.132C34.167,26.537 33.7,26 33.093,26Z" + android:fillColor="#ffffff"/> +</vector> diff --git a/app/src/main/res/drawable/ic_composer_rich_text_editor_close.xml b/app/src/main/res/drawable/ic_composer_rich_text_editor_close.xml new file mode 100644 index 000000000..cb80a6587 --- /dev/null +++ b/app/src/main/res/drawable/ic_composer_rich_text_editor_close.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="12dp" + android:height="12dp" + android:viewportWidth="12" + android:viewportHeight="12"> + <path + android:pathData="M10.403,2.53C10.696,2.237 10.696,1.763 10.403,1.47C10.111,1.177 9.636,1.177 9.343,1.47L5.946,4.867L2.549,1.47C2.256,1.177 1.781,1.177 1.488,1.47C1.195,1.763 1.195,2.237 1.488,2.53L4.885,5.927L1.343,9.47C1.05,9.763 1.05,10.237 1.343,10.53C1.636,10.823 2.11,10.823 2.403,10.53L5.946,6.988L9.488,10.53C9.781,10.823 10.256,10.823 10.549,10.53C10.842,10.237 10.842,9.763 10.549,9.47L7.006,5.927L10.403,2.53Z" + android:fillColor="@color/blue" /> +</vector> diff --git a/app/src/main/res/drawable/ic_composer_rich_text_editor_edit.xml b/app/src/main/res/drawable/ic_composer_rich_text_editor_edit.xml new file mode 100644 index 000000000..1c3ddde54 --- /dev/null +++ b/app/src/main/res/drawable/ic_composer_rich_text_editor_edit.xml @@ -0,0 +1,12 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="12dp" + android:height="12dp" + android:viewportWidth="12" + android:viewportHeight="12"> + <path + android:pathData="M2.649,7.355C2.655,7.316 2.672,7.28 2.699,7.251L8.404,1.064C8.479,0.983 8.605,0.978 8.686,1.053L9.863,2.138C9.944,2.213 9.949,2.339 9.874,2.42L4.169,8.607C4.143,8.636 4.108,8.656 4.069,8.665L2.668,9.005C2.529,9.039 2.401,8.92 2.423,8.779L2.649,7.355Z" + android:fillColor="@color/blue" /> + <path + android:pathData="M1.75,9.443C1.336,9.443 1,9.779 1,10.193C1,10.608 1.336,10.943 1.75,10.943L10.75,10.943C11.164,10.943 11.5,10.608 11.5,10.193C11.5,9.779 11.164,9.443 10.75,9.443L1.75,9.443Z" + android:fillColor="@color/blue" /> +</vector> diff --git a/app/src/main/res/drawable/ic_composer_rich_text_save.xml b/app/src/main/res/drawable/ic_composer_rich_text_save.xml new file mode 100644 index 000000000..63eb8d2c4 --- /dev/null +++ b/app/src/main/res/drawable/ic_composer_rich_text_save.xml @@ -0,0 +1,16 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M18,18m-18,0a18,18 0,1 1,36 0a18,18 0,1 1,-36 0" + android:fillColor="?colorPrimary"/> + <path + android:pathData="M9.818,18.787L14.705,23.818L26.182,12" + android:strokeLineJoin="round" + android:strokeWidth="2.5" + android:fillColor="#00000000" + android:strokeColor="?colorOnPrimary" + android:strokeLineCap="round"/> +</vector> diff --git a/app/src/main/res/drawable/ic_composer_strikethrough.xml b/app/src/main/res/drawable/ic_composer_strikethrough.xml new file mode 100644 index 000000000..593cd3aca --- /dev/null +++ b/app/src/main/res/drawable/ic_composer_strikethrough.xml @@ -0,0 +1,12 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="44dp" + android:height="44dp" + android:viewportWidth="44" + android:viewportHeight="44"> + <path + android:pathData="M24.897,17.154C24.235,15.821 22.876,15.21 21.374,15.372C19.05,15.622 18.44,17.423 18.722,18.592C19.032,19.872 20.046,20.37 21.839,20.826H29.92C30.517,20.826 31,21.351 31,22C31,22.648 30.517,23.174 29.92,23.174H14.08C13.483,23.174 13,22.648 13,22C13,21.351 13.483,20.826 14.08,20.826H17.355C17.041,20.377 16.791,19.839 16.633,19.189C16.003,16.581 17.554,13.424 21.16,13.036C23.285,12.807 25.615,13.661 26.798,16.038C27.081,16.608 26.886,17.32 26.361,17.629C25.836,17.937 25.181,17.725 24.897,17.154Z" + android:fillColor="@color/blue" /> + <path + android:pathData="M25.427,25.13H27.67C27.888,26.306 27.721,27.56 27.05,28.632C26.114,30.125 24.37,31 21.985,31C18.076,31 16.279,28.584 15.912,26.986C15.768,26.357 16.12,25.72 16.698,25.563C17.277,25.406 17.863,25.788 18.008,26.417C18.119,26.902 19.002,28.652 21.985,28.652C23.907,28.652 24.854,27.965 25.264,27.31C25.642,26.707 25.708,25.909 25.427,25.13Z" + android:fillColor="@color/blue" /> +</vector> diff --git a/app/src/main/res/drawable/ic_composer_underlined.xml b/app/src/main/res/drawable/ic_composer_underlined.xml new file mode 100644 index 000000000..2e3d997d7 --- /dev/null +++ b/app/src/main/res/drawable/ic_composer_underlined.xml @@ -0,0 +1,12 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="44dp" + android:height="44dp" + android:viewportWidth="44" + android:viewportHeight="44"> + <group> + <clip-path android:pathData="M10,10h24v24h-24z" /> + <path + android:pathData="M22.79,26.95C25.82,26.56 28,23.84 28,20.79V14.25C28,13.56 27.44,13 26.75,13C26.06,13 25.5,13.56 25.5,14.25V20.9C25.5,22.57 24.37,24.09 22.73,24.42C20.48,24.89 18.5,23.17 18.5,21V14.25C18.5,13.56 17.94,13 17.25,13C16.56,13 16,13.56 16,14.25V21C16,24.57 19.13,27.42 22.79,26.95ZM15,30C15,30.55 15.45,31 16,31H28C28.55,31 29,30.55 29,30C29,29.45 28.55,29 28,29H16C15.45,29 15,29.45 15,30Z" + android:fillColor="@color/blue" /> + </group> +</vector> diff --git a/app/src/main/res/drawable/ic_composer_unindent.xml b/app/src/main/res/drawable/ic_composer_unindent.xml new file mode 100644 index 000000000..f7f383c49 --- /dev/null +++ b/app/src/main/res/drawable/ic_composer_unindent.xml @@ -0,0 +1,12 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="44dp" + android:height="44dp" + android:viewportWidth="44" + android:viewportHeight="44"> + <group> + <clip-path android:pathData="M10,10h24v24h-24z" /> + <path + android:pathData="M22,27H30C30.55,27 31,26.55 31,26C31,25.45 30.55,25 30,25H22C21.45,25 21,25.45 21,26C21,26.55 21.45,27 22,27ZM13.35,22.35L16.14,25.14C16.46,25.46 17,25.24 17,24.79V19.21C17,18.76 16.46,18.54 16.15,18.86L13.36,21.65C13.16,21.84 13.16,22.16 13.35,22.35ZM14,31H30C30.55,31 31,30.55 31,30C31,29.45 30.55,29 30,29H14C13.45,29 13,29.45 13,30C13,30.55 13.45,31 14,31ZM13,14C13,14.55 13.45,15 14,15H30C30.55,15 31,14.55 31,14C31,13.45 30.55,13 30,13H14C13.45,13 13,13.45 13,14ZM22,19H30C30.55,19 31,18.55 31,18C31,17.45 30.55,17 30,17H22C21.45,17 21,17.45 21,18C21,18.55 21.45,19 22,19ZM22,23H30C30.55,23 31,22.55 31,22C31,21.45 30.55,21 30,21H22C21.45,21 21,21.45 21,22C21,22.55 21.45,23 22,23Z" + android:fillColor="@color/blue" /> + </group> +</vector> diff --git a/app/src/main/res/drawable/ic_quote.xml b/app/src/main/res/drawable/ic_quote.xml new file mode 100644 index 000000000..0689651f1 --- /dev/null +++ b/app/src/main/res/drawable/ic_quote.xml @@ -0,0 +1,14 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="20dp" + android:height="14dp" + android:viewportWidth="20" + android:viewportHeight="14"> + <path + android:pathData="M19,5H1M19,1H1M10,9H1M10,13H1" + android:strokeLineJoin="round" + android:strokeWidth="2" + android:fillColor="#00000000" + android:fillType="evenOdd" + android:strokeColor="#9E9E9E" + android:strokeLineCap="round"/> +</vector> diff --git a/app/src/main/res/drawable/ic_rich_composer_add.xml b/app/src/main/res/drawable/ic_rich_composer_add.xml new file mode 100644 index 000000000..a5961c498 --- /dev/null +++ b/app/src/main/res/drawable/ic_rich_composer_add.xml @@ -0,0 +1,15 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M18,18m-18,0a18,18 0,1 1,36 0a18,18 0,1 1,-36 0" + android:fillColor="@color/blue"/> + <path + android:pathData="M11.251,18H24.751M18.001,11.25V24.75" + android:strokeWidth="2" + android:fillColor="#00000000" + android:strokeColor="@color/blue" + android:strokeLineCap="round"/> +</vector> diff --git a/app/src/main/res/drawable/ic_rich_composer_send.xml b/app/src/main/res/drawable/ic_rich_composer_send.xml new file mode 100644 index 000000000..b3df6e92d --- /dev/null +++ b/app/src/main/res/drawable/ic_rich_composer_send.xml @@ -0,0 +1,12 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="36dp" + android:viewportWidth="36" + android:viewportHeight="36"> + <path + android:pathData="M18,18m-18,0a18,18 0,1 1,36 0a18,18 0,1 1,-36 0" + android:fillColor="?colorPrimary"/> + <path + android:pathData="M27.83,19.085L12.26,26.867C11.21,27.391 10.119,26.266 10.632,25.24C10.632,25.24 12.561,21.343 13.092,20.322C13.623,19.301 14.231,19.124 19.874,18.395C20.083,18.368 20.253,18.21 20.253,18C20.253,17.79 20.083,17.632 19.874,17.605C14.231,16.876 13.623,16.699 13.092,15.678C12.561,14.658 10.632,10.76 10.632,10.76C10.119,9.734 11.21,8.609 12.26,9.133L27.83,16.915C28.725,17.362 28.725,18.638 27.83,19.085Z" + android:fillColor="?colorOnPrimary"/> +</vector> diff --git a/app/src/main/res/layout/composer_rich_text_layout.xml b/app/src/main/res/layout/composer_rich_text_layout.xml new file mode 100644 index 000000000..a8f0b4baa --- /dev/null +++ b/app/src/main/res/layout/composer_rich_text_layout.xml @@ -0,0 +1,206 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/composerLayout" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <FrameLayout + android:id="@+id/bottomSheetHandle" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent"> + + <View + android:layout_width="36dp" + android:layout_height="5dp" + android:layout_gravity="center_horizontal" + android:layout_marginTop="8dp" + /> + + </FrameLayout> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/composerLayoutContent" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <ImageButton + android:id="@+id/attachmentButton" + android:layout_width="60dp" + android:layout_height="56dp" + android:background="?android:attr/selectableItemBackgroundBorderless" + android:contentDescription="Send Files" + android:src="@drawable/ic_rich_composer_add" + app:layout_constraintVertical_bias="1" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_goneMarginBottom="56dp" + tools:ignore="MissingPrefix,RtlSymmetry" /> + + <!-- Constraints are updated programmatically --> + <FrameLayout + android:id="@+id/composerEditTextOuterBorder" + android:layout_width="0dp" + android:layout_height="0dp" + android:minHeight="40dp" + android:layout_marginTop="8dp" + android:layout_marginBottom="8dp" + android:layout_marginHorizontal="12dp" + app:layout_constraintVertical_bias="0" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toTopOf="@id/sendButton" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" /> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/composerModeIconView" + android:layout_width="11dp" + android:layout_height="11dp" + tools:src="@drawable/ic_quote" + android:layout_marginStart="12dp" + app:layout_constraintTop_toTopOf="@id/composerModeTitleView" + app:layout_constraintBottom_toBottomOf="@id/composerModeTitleView" + app:layout_constraintStart_toStartOf="@id/composerEditTextOuterBorder" + app:tint="@color/blue" /> + + <TextView android:id="@+id/composerModeTitleView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="6dp" + android:layout_marginTop="8dp" + android:paddingBottom="2dp" + android:fontFamily="sans-serif-medium" + tools:text="Editing" + app:layout_constraintTop_toTopOf="@id/composerEditTextOuterBorder" + app:layout_constraintStart_toEndOf="@id/composerModeIconView" /> + + <ImageButton android:id="@+id/composerModeCloseView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:src="@drawable/ic_composer_rich_text_editor_close" + android:background="?android:selectableItemBackground" + android:layout_marginEnd="12dp" + app:layout_constraintTop_toTopOf="@id/composerModeIconView" + app:layout_constraintEnd_toEndOf="@id/composerEditTextOuterBorder" /> + + <androidx.constraintlayout.widget.Barrier + android:id="@+id/composerModeBarrier" + android:layout_width="0dp" + android:layout_height="0dp" + app:barrierDirection="bottom" + app:constraint_referenced_ids="composerModeIconView,composerModeTitleView,composerModeCloseView" /> + + <androidx.constraintlayout.widget.Group + android:id="@+id/composerModeGroup" + android:layout_width="0dp" + android:layout_height="0dp" + android:visibility="gone" + tools:visibility="visible" + app:constraint_referenced_ids="composerModeIconView,composerModeTitleView,composerModeCloseView" /> + + <io.element.android.wysiwyg.EditorEditText + android:id="@+id/richTextComposerEditText" + android:layout_width="0dp" + android:layout_height="0dp" + app:layout_constraintHeight_default="wrap" + android:gravity="top" + android:hint="Room message placeholder" + android:nextFocusLeft="@id/richTextComposerEditText" + android:nextFocusUp="@id/richTextComposerEditText" + android:layout_marginStart="12dp" + android:imeOptions="flagNoExtractUi" + app:layout_constraintVertical_bias="0" + app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder" + app:layout_constraintEnd_toStartOf="@id/composerFullScreenButton" + app:layout_constraintStart_toStartOf="@id/composerEditTextOuterBorder" + app:layout_constraintTop_toBottomOf="@id/composerModeBarrier" + app:bulletRadius="4sp" + app:bulletGap="8sp" + app:codeBlockBackgroundDrawable="@drawable/bg_code_block" + app:inlineCodeSingleLineBg="@drawable/bg_inline_code_single_line" + app:inlineCodeMultiLineBgLeft="@drawable/bg_inline_code_multi_line_left" + app:inlineCodeMultiLineBgMid="@drawable/bg_inline_code_multi_line_mid" + app:inlineCodeMultiLineBgRight="@drawable/bg_inline_code_multi_line_right" + tools:text="@tools:sample/lorem/random" /> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/plainTextComposerEditText" + android:layout_width="0dp" + android:layout_height="0dp" + app:layout_constraintHeight_default="wrap" + android:visibility="gone" + android:hint="Room placeholder" + android:nextFocusLeft="@id/plainTextComposerEditText" + android:nextFocusUp="@id/plainTextComposerEditText" + android:layout_marginStart="12dp" + android:gravity="top" + android:imeOptions="flagNoExtractUi" + app:layout_constraintVertical_bias="0" + app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder" + app:layout_constraintEnd_toStartOf="@id/composerFullScreenButton" + app:layout_constraintStart_toStartOf="@id/composerEditTextOuterBorder" + app:layout_constraintTop_toBottomOf="@id/composerModeBarrier" + tools:text="@tools:sample/lorem/random" /> + + <ImageButton + android:id="@+id/composerFullScreenButton" + android:layout_width="40dp" + android:layout_height="40dp" + android:layout_marginEnd="4dp" + app:layout_constraintEnd_toEndOf="@id/composerEditTextOuterBorder" + app:layout_constraintTop_toBottomOf="@id/composerModeBarrier" + app:layout_constraintBottom_toBottomOf="@id/composerEditTextOuterBorder" + app:layout_constraintVertical_bias="0" + android:src="@drawable/ic_composer_full_screen" + android:background="?android:attr/selectableItemBackgroundBorderless" + android:contentDescription="@string/rich_text_editor_full_screen_toggle" /> + + <ImageButton + android:id="@+id/sendButton" + android:layout_width="56dp" + android:layout_height="56dp" + android:paddingEnd="4dp" + android:contentDescription="@string/action_send" + android:scaleType="center" + android:src="@drawable/ic_rich_composer_send" + android:visibility="invisible" + android:background="?android:attr/selectableItemBackgroundBorderless" + app:layout_constraintTop_toBottomOf="@id/composerEditTextOuterBorder" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintVertical_bias="1" + tools:ignore="MissingPrefix,RtlSymmetry" + tools:visibility="visible" /> + + <HorizontalScrollView android:id="@+id/richTextMenuScrollView" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:minHeight="52dp" + android:requiresFadingEdge="horizontal" + android:fadingEdgeLength="28dp" + app:layout_constraintTop_toBottomOf="@id/composerEditTextOuterBorder" + app:layout_constraintStart_toEndOf="@id/attachmentButton" + app:layout_constraintEnd_toStartOf="@id/sendButton" + app:layout_constraintBottom_toBottomOf="parent" + android:fillViewport="true"> + + <LinearLayout + android:id="@+id/richTextMenu" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:orientation="horizontal" + android:paddingVertical="4dp"> + + </LinearLayout> + + </HorizontalScrollView> + + </androidx.constraintlayout.widget.ConstraintLayout> + +</LinearLayout> diff --git a/app/src/main/res/layout/view_preview_post.xml b/app/src/main/res/layout/view_preview_post.xml index 13f7ae6b1..8f3a8baa7 100644 --- a/app/src/main/res/layout/view_preview_post.xml +++ b/app/src/main/res/layout/view_preview_post.xml @@ -51,20 +51,37 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> - <org.futo.circles.view.MarkdownEditText - android:id="@+id/etTextPost" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:background="@null" - android:gravity="top" - android:hint="@string/enter_your_message_here" - android:inputType="textCapSentences|textMultiLine" - android:paddingStart="12dp" - android:paddingEnd="12dp" - app:layout_constraintBottom_toTopOf="@id/lMediaContent" + <org.futo.circles.view.RichTextComposerLayout + android:id="@+id/richTextComposerLayout" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:minHeight="56dp" + android:transitionName="composer" app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHeight_default="wrap" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintTop_toTopOf="parent" + tools:visibility="visible" /> + + <!-- <io.element.android.wysiwyg.EditorEditText--> + <!-- android:id="@+id/etTextPost"--> + <!-- android:layout_width="0dp"--> + <!-- android:layout_height="0dp"--> + <!-- android:gravity="top"--> + <!-- android:hint="Hint"--> + <!-- app:bulletGap="8sp"--> + <!-- app:bulletRadius="4sp"--> + <!-- app:codeBlockBackgroundDrawable="@drawable/ic_text"--> + <!-- app:inlineCodeMultiLineBgLeft="@drawable/ic_text"--> + <!-- app:inlineCodeMultiLineBgMid="@drawable/ic_text"--> + <!-- app:inlineCodeMultiLineBgRight="@drawable/ic_text"--> + <!-- app:inlineCodeSingleLineBg="@drawable/ic_text"--> + <!-- app:layout_constraintBottom_toTopOf="@id/lMediaContent"--> + <!-- app:layout_constraintEnd_toEndOf="parent"--> + <!-- app:layout_constraintHeight_default="wrap"--> + <!-- app:layout_constraintStart_toStartOf="parent"--> + <!-- app:layout_constraintTop_toTopOf="parent"--> + <!-- tools:text="@tools:sample/lorem/random" />--> <include @@ -75,8 +92,7 @@ android:layout_marginTop="8dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@id/etTextPost" /> + app:layout_constraintStart_toStartOf="parent" /> <com.google.android.material.imageview.ShapeableImageView android:id="@+id/ivRemoveImage" @@ -91,7 +107,7 @@ app:layout_constraintEnd_toEndOf="@id/lMediaContent" app:layout_constraintTop_toTopOf="@id/lMediaContent" app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.App.CornerSize50Percent" /> - + </androidx.constraintlayout.widget.ConstraintLayout> </ScrollView> diff --git a/app/src/main/res/layout/view_rich_text_menu_button.xml b/app/src/main/res/layout/view_rich_text_menu_button.xml new file mode 100644 index 000000000..b99a29da2 --- /dev/null +++ b/app/src/main/res/layout/view_rich_text_menu_button.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<ImageButton xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="@dimen/rich_text_composer_menu_item_size" + android:layout_height="@dimen/rich_text_composer_menu_item_size" + android:layout_marginHorizontal="2dp" + android:background="@drawable/bg_rich_text_menu_button" + app:tint="@color/selector_rich_text_menu_icon" + tools:src="@drawable/ic_composer_bold" + tools:ignore="ContentDescription" /> diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index ac9eed3a5..be23a2f4f 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -1,6 +1,8 @@ <?xml version="1.0" encoding="utf-8"?> <resources> + <attr name="vctr_content_quaternary" format="color" /> + <declare-styleable name="GroupPostHeaderView"> <attr name="optionsVisible" format="boolean" /> </declare-styleable> diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 57b80004b..b92aaf03d 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -5,4 +5,8 @@ <dimen name="post_text_side_margin">24dp</dimen> <dimen name="circle_icon_size">100dp</dimen> <dimen name="profile_avatar_size">50dp</dimen> + + <dimen name="rich_text_composer_corner_radius_single_line">28dp</dimen> + <dimen name="rich_text_composer_corner_radius_expanded">14dp</dimen> + <dimen name="rich_text_composer_menu_item_size">44dp</dimen> </resources> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index eade56ad3..fa73109f6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -216,6 +216,26 @@ <string name="help">Help</string> <string name="optional_request_message">Optional: Request message</string> + <!-- Rich text editor --> + <string name="rich_text_editor_format_bold">Apply bold format</string> + <string name="rich_text_editor_format_italic">Apply italic format</string> + <string name="rich_text_editor_format_strikethrough">Apply strikethrough format</string> + <string name="rich_text_editor_format_underline">Apply underline format</string> + <string name="rich_text_editor_link">Set link</string> + <string name="rich_text_editor_numbered_list">Toggle numbered list</string> + <string name="rich_text_editor_bullet_list">Toggle bullet list</string> + <string name="rich_text_editor_indent">Indent</string> + <string name="rich_text_editor_unindent">Unindent</string> + <string name="rich_text_editor_quote">Toggle quote</string> + <string name="rich_text_editor_inline_code">Apply inline code format</string> + <string name="rich_text_editor_code_block">Toggle code block</string> + <string name="rich_text_editor_full_screen_toggle">Toggle full screen mode</string> + <string name="action_save">Save</string> + <string name="action_send">Send</string> + <string name="editing">Editing</string> + <string name="quoting">Quoting</string> + <string name="replying_to">Replying to %s</string> + <string-array name="report_categories"> <item>@string/crude_language</item> <item>@string/copyright_violation</item> diff --git a/build.gradle b/build.gradle index 924f2bdbc..5eb8219f8 100644 --- a/build.gradle +++ b/build.gradle @@ -12,7 +12,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.1.3' + classpath 'com.android.tools.build:gradle:8.1.4' classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.21' classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$androidx_nav_version" classpath 'com.google.gms:google-services:4.4.0' -- GitLab