mirror of
https://github.com/anyproto/anytype-kotlin.git
synced 2025-06-08 13:57:10 +09:00
* #574: mention suggests widget * #574: add footer decorator * #574: suggester widget * #574: hide mention suggester widget * #574: fixes * #574: tests * #574: show suggests * #574: fix tests * #574: marks adjust method * #574: mention events * #574: suggest widget fixes * #574: render new mention * #574: fixes * #574: render mention * #574: fixes + tests * #574: create new page from suggester * #574: suggester icon * #574: design fixes * #574: fixes * #574: fixes * #574: fixes * #574: fix tests * #574: fixes * #574: fixes * #574: fixes * #574: fixes * #574: fix position + empty emoji * #574: cursor position * #574: fix mention filter text * #574: code style * #574: fix test * #574: pr fixes * #574: start tests * #574: fixes * #574: tests off
This commit is contained in:
parent
badf961d5b
commit
832fc6d315
45 changed files with 1434 additions and 150 deletions
|
@ -20,6 +20,7 @@ import com.agileburo.anytype.domain.icon.DocumentEmojiIconProvider
|
|||
import com.agileburo.anytype.domain.misc.UrlBuilder
|
||||
import com.agileburo.anytype.domain.page.*
|
||||
import com.agileburo.anytype.domain.page.bookmark.SetupBookmark
|
||||
import com.agileburo.anytype.domain.page.navigation.GetListPages
|
||||
import com.agileburo.anytype.mocking.MockDataFactory
|
||||
import com.agileburo.anytype.presentation.page.DocumentExternalEventReducer
|
||||
import com.agileburo.anytype.presentation.page.Editor
|
||||
|
@ -73,6 +74,8 @@ open class EditorTestSetup {
|
|||
@Mock
|
||||
lateinit var unlinkBlocks: UnlinkBlocks
|
||||
@Mock
|
||||
lateinit var getListPages: GetListPages
|
||||
@Mock
|
||||
lateinit var duplicateBlock: DuplicateBlock
|
||||
@Mock
|
||||
lateinit var updateTextStyle: UpdateTextStyle
|
||||
|
@ -164,6 +167,7 @@ open class EditorTestSetup {
|
|||
counter = Counter.Default(),
|
||||
toggleStateHolder = ToggleStateHolder.Default()
|
||||
),
|
||||
getListPages = getListPages,
|
||||
interactor = Orchestrator(
|
||||
createBlock = createBlock,
|
||||
splitBlock = splitBlock,
|
||||
|
|
|
@ -16,6 +16,7 @@ import com.agileburo.anytype.domain.icon.DocumentEmojiIconProvider
|
|||
import com.agileburo.anytype.domain.misc.UrlBuilder
|
||||
import com.agileburo.anytype.domain.page.*
|
||||
import com.agileburo.anytype.domain.page.bookmark.SetupBookmark
|
||||
import com.agileburo.anytype.domain.page.navigation.GetListPages
|
||||
import com.agileburo.anytype.presentation.page.DocumentExternalEventReducer
|
||||
import com.agileburo.anytype.presentation.page.Editor
|
||||
import com.agileburo.anytype.presentation.page.PageViewModelFactory
|
||||
|
@ -61,7 +62,8 @@ object PageModule {
|
|||
urlBuilder: UrlBuilder,
|
||||
renderer: DefaultBlockViewRenderer,
|
||||
archiveDocument: ArchiveDocument,
|
||||
interactor: Orchestrator
|
||||
interactor: Orchestrator,
|
||||
getListPages: GetListPages
|
||||
): PageViewModelFactory = PageViewModelFactory(
|
||||
openPage = openPage,
|
||||
closePage = closePage,
|
||||
|
@ -74,9 +76,15 @@ object PageModule {
|
|||
urlBuilder = urlBuilder,
|
||||
renderer = renderer,
|
||||
archiveDocument = archiveDocument,
|
||||
interactor = interactor
|
||||
interactor = interactor,
|
||||
getListPages = getListPages
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
@PerScreen
|
||||
@Provides
|
||||
fun getListPages(repo: BlockRepository): GetListPages = GetListPages(repo = repo)
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@PerScreen
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.text.Editable
|
|||
import android.text.Spanned
|
||||
import com.agileburo.anytype.core_ui.common.Span
|
||||
import com.agileburo.anytype.core_ui.common.ThemeColor
|
||||
import com.agileburo.anytype.core_ui.widgets.text.MentionSpan
|
||||
import com.agileburo.anytype.domain.block.model.Block.Content.Text.Mark
|
||||
import com.agileburo.anytype.domain.ext.overlap
|
||||
import com.agileburo.anytype.domain.misc.Overlap
|
||||
|
@ -47,6 +48,11 @@ fun Editable.extractMarks(): List<Mark> = getSpans(0, length, Span::class.java).
|
|||
type = Mark.Type.LINK,
|
||||
param = span.url
|
||||
)
|
||||
is MentionSpan -> Mark(
|
||||
range = getSpanStart(span)..getSpanEnd(span),
|
||||
type = Mark.Type.MENTION,
|
||||
param = span.param
|
||||
)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import android.os.Bundle
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import android.view.animation.LinearInterpolator
|
||||
import android.view.animation.OvershootInterpolator
|
||||
import android.widget.Button
|
||||
import android.widget.FrameLayout
|
||||
|
@ -20,9 +21,13 @@ import android.widget.TextView
|
|||
import androidx.activity.addCallback
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.constraintlayout.widget.ConstraintSet
|
||||
import androidx.core.animation.doOnEnd
|
||||
import androidx.core.animation.doOnStart
|
||||
import androidx.core.view.get
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.core.view.updatePadding
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModelProviders
|
||||
|
@ -30,6 +35,8 @@ import androidx.lifecycle.lifecycleScope
|
|||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.transition.ChangeBounds
|
||||
import androidx.transition.Fade
|
||||
import androidx.transition.TransitionManager
|
||||
import androidx.transition.TransitionSet
|
||||
import com.agileburo.anytype.BuildConfig
|
||||
import com.agileburo.anytype.R
|
||||
import com.agileburo.anytype.core_ui.common.Alignment
|
||||
|
@ -50,10 +57,12 @@ import com.agileburo.anytype.core_ui.reactive.clicks
|
|||
import com.agileburo.anytype.core_ui.state.ControlPanelState
|
||||
import com.agileburo.anytype.core_ui.tools.ClipboardInterceptor
|
||||
import com.agileburo.anytype.core_ui.tools.FirstItemInvisibilityDetector
|
||||
import com.agileburo.anytype.core_ui.tools.MentionFooterItemDecorator
|
||||
import com.agileburo.anytype.core_ui.tools.OutsideClickDetector
|
||||
import com.agileburo.anytype.core_ui.widgets.ActionItemType
|
||||
import com.agileburo.anytype.core_utils.common.EventWrapper
|
||||
import com.agileburo.anytype.core_utils.ext.*
|
||||
import com.agileburo.anytype.core_utils.ext.PopupExtensions.calculateRectInWindow
|
||||
import com.agileburo.anytype.di.common.componentManager
|
||||
import com.agileburo.anytype.domain.block.model.Block.Content.Text
|
||||
import com.agileburo.anytype.domain.common.Id
|
||||
|
@ -129,6 +138,10 @@ open class PageFragment :
|
|||
)
|
||||
}
|
||||
|
||||
private val footerMentionDecorator by lazy {
|
||||
MentionFooterItemDecorator(screen = screen)
|
||||
}
|
||||
|
||||
private val vm by lazy {
|
||||
ViewModelProviders
|
||||
.of(this, factory)
|
||||
|
@ -198,7 +211,8 @@ open class PageFragment :
|
|||
onTitleTextInputClicked = vm::onTitleTextInputClicked,
|
||||
onClickListener = vm::onClickListener,
|
||||
clipboardInterceptor = this,
|
||||
anytypeContextMenuListener = anytypeContextMenuListener
|
||||
anytypeContextMenuListener = anytypeContextMenuListener,
|
||||
onMentionEvent = vm::onMentionEvent
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -448,6 +462,11 @@ open class PageFragment :
|
|||
vm.onBackButtonPressed()
|
||||
}.launchIn(lifecycleScope)
|
||||
|
||||
mentionSuggesterToolbar.setupClicks(
|
||||
mentionClick = vm::onMentionSuggestClick,
|
||||
newPageClick = vm::onAddMentionNewPageClicked
|
||||
)
|
||||
|
||||
lifecycleScope.launch {
|
||||
styleToolbar.events.collect { event ->
|
||||
when (event) {
|
||||
|
@ -843,6 +862,60 @@ open class PageFragment :
|
|||
recycler.updatePadding(bottom = dimen(R.dimen.default_toolbar_height))
|
||||
}
|
||||
}
|
||||
|
||||
state.mentionToolbar.apply {
|
||||
if (isVisible) {
|
||||
if (!mentionSuggesterToolbar.isVisible) {
|
||||
showMentionToolbar(this)
|
||||
}
|
||||
if (updateList) {
|
||||
mentionSuggesterToolbar.addItems(mentions)
|
||||
}
|
||||
mentionFilter?.let {
|
||||
mentionSuggesterToolbar.updateFilter(it)
|
||||
}
|
||||
} else {
|
||||
mentionSuggesterToolbar.invisible()
|
||||
recycler.removeItemDecoration(footerMentionDecorator)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showMentionToolbar(state: ControlPanelState.Toolbar.MentionToolbar) {
|
||||
state.cursorCoordinate?.let { cursorCoordinate ->
|
||||
val parentBottom = calculateRectInWindow(recycler).bottom
|
||||
val toolbarHeight = mentionSuggesterToolbar.getMentionSuggesterWidgetMinHeight()
|
||||
val minPosY = parentBottom - toolbarHeight
|
||||
|
||||
if (minPosY <= cursorCoordinate) {
|
||||
val scrollY = (parentBottom - minPosY) - (parentBottom - cursorCoordinate)
|
||||
recycler.addItemDecoration(footerMentionDecorator)
|
||||
recycler.post {
|
||||
recycler.smoothScrollBy(0, scrollY)
|
||||
}
|
||||
}
|
||||
mentionSuggesterToolbar.updateLayoutParams<ConstraintLayout.LayoutParams> {
|
||||
height = toolbarHeight
|
||||
}
|
||||
val set = ConstraintSet().apply {
|
||||
clone(sheet)
|
||||
setVisibility(R.id.mentionSuggesterToolbar, View.VISIBLE)
|
||||
connect(
|
||||
R.id.mentionSuggesterToolbar,
|
||||
ConstraintSet.BOTTOM,
|
||||
R.id.sheet,
|
||||
ConstraintSet.BOTTOM
|
||||
)
|
||||
}
|
||||
val transitionSet = TransitionSet().apply {
|
||||
addTransition(ChangeBounds())
|
||||
duration = SHOW_MENTION_TRANSITION_DURATION
|
||||
interpolator = LinearInterpolator()
|
||||
ordering = TransitionSet.ORDERING_TOGETHER
|
||||
}
|
||||
TransitionManager.beginDelayedTransition(sheet, transitionSet)
|
||||
set.applyTo(sheet)
|
||||
}
|
||||
}
|
||||
|
||||
private fun enterScrollAndMove() {
|
||||
|
@ -1118,6 +1191,7 @@ open class PageFragment :
|
|||
const val FAB_SHOW_ANIMATION_START_DELAY = 250L
|
||||
const val FAB_SHOW_ANIMATION_DURATION = 100L
|
||||
|
||||
const val SHOW_MENTION_TRANSITION_DURATION = 150L
|
||||
const val SELECT_BUTTON_SHOW_ANIMATION_DURATION = 200L
|
||||
const val SELECT_BUTTON_HIDE_ANIMATION_DURATION = 200L
|
||||
const val SELECT_BUTTON_ANIMATION_PROPERTY = "translationY"
|
||||
|
|
|
@ -101,6 +101,16 @@
|
|||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<com.agileburo.anytype.core_ui.widgets.toolbar.MentionToolbar
|
||||
android:id="@+id/mentionSuggesterToolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="invisible"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<com.agileburo.anytype.core_ui.widgets.toolbar.PageBottomToolbar
|
||||
|
|
|
@ -134,7 +134,8 @@ fun Markup.toSpannable(
|
|||
imagePadding = mentionImagePadding,
|
||||
//todo Setting up default drawable, will be fixed in feature
|
||||
mResourceId = R.drawable.ic_mention_deafult,
|
||||
bitmap = null
|
||||
bitmap = null,
|
||||
param = mark.param
|
||||
),
|
||||
mark.from,
|
||||
mark.to,
|
||||
|
@ -152,7 +153,11 @@ fun Markup.toSpannable(
|
|||
Markup.DEFAULT_SPANNABLE_FLAG
|
||||
)
|
||||
} else {
|
||||
Timber.e("Get Mention span without param!")
|
||||
if (context == null) {
|
||||
Timber.e("Context for MentionSpan is null!")
|
||||
} else {
|
||||
Timber.e("Get MentionSpan without param!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,10 +3,14 @@ package com.agileburo.anytype.core_ui.extensions
|
|||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import com.agileburo.anytype.core_ui.R
|
||||
import com.agileburo.anytype.core_ui.widgets.text.TextInputWidget
|
||||
import com.agileburo.anytype.core_utils.ext.PopupExtensions
|
||||
import com.agileburo.anytype.core_utils.ext.PopupExtensions.calculateRectInWindow
|
||||
|
||||
fun Context.toast(
|
||||
msg: CharSequence,
|
||||
|
@ -42,4 +46,16 @@ fun LinearLayout.addVerticalDivider(
|
|||
}
|
||||
)
|
||||
|
||||
fun EditText.cursorYBottomCoordinate(): Int {
|
||||
with(this.layout) {
|
||||
val pos = selectionStart
|
||||
val line = getLineForOffset(pos)
|
||||
val baseLine = getLineBaseline(line)
|
||||
val ascent = getLineAscent(line)
|
||||
val rect = calculateRectInWindow(this@cursorYBottomCoordinate)
|
||||
|
||||
return baseLine + ascent + rect.bottom - scrollY
|
||||
}
|
||||
}
|
||||
|
||||
fun TextView.range() : IntRange = selectionStart..selectionEnd
|
|
@ -74,7 +74,8 @@ class BlockAdapter(
|
|||
private val onToggleClicked: (String) -> Unit,
|
||||
private val onMarkupActionClicked: (Markup.Type, IntRange) -> Unit,
|
||||
private val clipboardInterceptor: ClipboardInterceptor,
|
||||
private val anytypeContextMenuListener: ((AnytypeContextMenuEvent) -> Unit)? = null
|
||||
private val anytypeContextMenuListener: ((AnytypeContextMenuEvent) -> Unit)? = null,
|
||||
private val onMentionEvent: (MentionEvent) -> Unit
|
||||
) : RecyclerView.Adapter<BlockViewHolder>() {
|
||||
|
||||
val views: List<BlockView> get() = blocks
|
||||
|
@ -414,7 +415,8 @@ class BlockAdapter(
|
|||
payloads = payloads.typeOf(),
|
||||
item = blocks[position],
|
||||
onTextChanged = onParagraphTextChanged,
|
||||
onSelectionChanged = onSelectionChanged
|
||||
onSelectionChanged = onSelectionChanged,
|
||||
clicked = onClickListener
|
||||
)
|
||||
}
|
||||
is BlockViewHolder.Bulleted -> {
|
||||
|
@ -422,7 +424,8 @@ class BlockAdapter(
|
|||
payloads = payloads.typeOf(),
|
||||
item = blocks[position],
|
||||
onTextChanged = onParagraphTextChanged,
|
||||
onSelectionChanged = onSelectionChanged
|
||||
onSelectionChanged = onSelectionChanged,
|
||||
clicked = onClickListener
|
||||
)
|
||||
}
|
||||
is BlockViewHolder.Checkbox -> {
|
||||
|
@ -430,7 +433,8 @@ class BlockAdapter(
|
|||
payloads = payloads.typeOf(),
|
||||
item = blocks[position],
|
||||
onTextChanged = onParagraphTextChanged,
|
||||
onSelectionChanged = onSelectionChanged
|
||||
onSelectionChanged = onSelectionChanged,
|
||||
clicked = onClickListener
|
||||
)
|
||||
}
|
||||
is BlockViewHolder.Title -> {
|
||||
|
@ -450,7 +454,8 @@ class BlockAdapter(
|
|||
payloads = payloads.typeOf(),
|
||||
item = blocks[position],
|
||||
onTextChanged = onParagraphTextChanged,
|
||||
onSelectionChanged = onSelectionChanged
|
||||
onSelectionChanged = onSelectionChanged,
|
||||
clicked = onClickListener
|
||||
)
|
||||
}
|
||||
is BlockViewHolder.HeaderOne -> {
|
||||
|
@ -458,7 +463,8 @@ class BlockAdapter(
|
|||
payloads = payloads.typeOf(),
|
||||
item = blocks[position],
|
||||
onTextChanged = onParagraphTextChanged,
|
||||
onSelectionChanged = onSelectionChanged
|
||||
onSelectionChanged = onSelectionChanged,
|
||||
clicked = onClickListener
|
||||
)
|
||||
}
|
||||
is BlockViewHolder.HeaderTwo -> {
|
||||
|
@ -466,7 +472,8 @@ class BlockAdapter(
|
|||
payloads = payloads.typeOf(),
|
||||
item = blocks[position],
|
||||
onTextChanged = onParagraphTextChanged,
|
||||
onSelectionChanged = onSelectionChanged
|
||||
onSelectionChanged = onSelectionChanged,
|
||||
clicked = onClickListener
|
||||
)
|
||||
}
|
||||
is BlockViewHolder.HeaderThree -> {
|
||||
|
@ -474,7 +481,8 @@ class BlockAdapter(
|
|||
payloads = payloads.typeOf(),
|
||||
item = blocks[position],
|
||||
onTextChanged = onParagraphTextChanged,
|
||||
onSelectionChanged = onSelectionChanged
|
||||
onSelectionChanged = onSelectionChanged,
|
||||
clicked = onClickListener
|
||||
)
|
||||
}
|
||||
is BlockViewHolder.Toggle -> {
|
||||
|
@ -482,7 +490,8 @@ class BlockAdapter(
|
|||
payloads = payloads.typeOf(),
|
||||
item = blocks[position],
|
||||
onTextChanged = onParagraphTextChanged,
|
||||
onSelectionChanged = onSelectionChanged
|
||||
onSelectionChanged = onSelectionChanged,
|
||||
clicked = onClickListener
|
||||
)
|
||||
}
|
||||
is BlockViewHolder.Highlight -> {
|
||||
|
@ -490,7 +499,8 @@ class BlockAdapter(
|
|||
payloads = payloads.typeOf(),
|
||||
item = blocks[position],
|
||||
onTextChanged = onParagraphTextChanged,
|
||||
onSelectionChanged = onSelectionChanged
|
||||
onSelectionChanged = onSelectionChanged,
|
||||
clicked = onClickListener
|
||||
)
|
||||
}
|
||||
is BlockViewHolder.File -> {
|
||||
|
@ -594,7 +604,8 @@ class BlockAdapter(
|
|||
payloads = payloads.typeOf(),
|
||||
item = blocks[position],
|
||||
onTextChanged = onParagraphTextChanged,
|
||||
onSelectionChanged = onSelectionChanged
|
||||
onSelectionChanged = onSelectionChanged,
|
||||
clicked = onClickListener
|
||||
)
|
||||
}
|
||||
else -> throw IllegalStateException("Unexpected view holder: $holder")
|
||||
|
@ -610,7 +621,8 @@ class BlockAdapter(
|
|||
onTextChanged = onParagraphTextChanged,
|
||||
onSelectionChanged = onSelectionChanged,
|
||||
onFocusChanged = onFocusChanged,
|
||||
clicked = onClickListener
|
||||
clicked = onClickListener,
|
||||
onMentionEvent = onMentionEvent
|
||||
)
|
||||
}
|
||||
is BlockViewHolder.Title -> {
|
||||
|
|
|
@ -107,7 +107,8 @@ sealed class BlockViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
|||
onTextChanged: (String, Editable) -> Unit,
|
||||
onSelectionChanged: (String, IntRange) -> Unit,
|
||||
onFocusChanged: (String, Boolean) -> Unit,
|
||||
clicked: (ListenerType) -> Unit
|
||||
clicked: (ListenerType) -> Unit,
|
||||
onMentionEvent: (MentionEvent) -> Unit
|
||||
) {
|
||||
|
||||
indentize(item)
|
||||
|
@ -141,7 +142,11 @@ sealed class BlockViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
|||
setFocus(item)
|
||||
if (item.isFocused) setCursor(item)
|
||||
|
||||
setupTextWatcher(onTextChanged, item)
|
||||
setupTextWatcher(
|
||||
onTextChanged = onTextChanged,
|
||||
onMentionEvent = onMentionEvent,
|
||||
item = item
|
||||
)
|
||||
|
||||
content.setOnFocusChangeListener { _, focused ->
|
||||
item.isFocused = focused
|
||||
|
@ -1116,9 +1121,10 @@ sealed class BlockViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
|||
payloads: List<Payload>,
|
||||
item: BlockView,
|
||||
onTextChanged: (String, Editable) -> Unit,
|
||||
onSelectionChanged: (String, IntRange) -> Unit
|
||||
onSelectionChanged: (String, IntRange) -> Unit,
|
||||
clicked: (ListenerType) -> Unit
|
||||
) {
|
||||
super.processChangePayload(payloads, item, onTextChanged, onSelectionChanged)
|
||||
super.processChangePayload(payloads, item, onTextChanged, onSelectionChanged, clicked)
|
||||
payloads.forEach { payload ->
|
||||
if (payload.changes.contains(NUMBER_CHANGED))
|
||||
number.text = "${(item as BlockView.Numbered).number}"
|
||||
|
@ -1275,10 +1281,11 @@ sealed class BlockViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
|||
payloads: List<Payload>,
|
||||
item: BlockView,
|
||||
onTextChanged: (String, Editable) -> Unit,
|
||||
onSelectionChanged: (String, IntRange) -> Unit
|
||||
onSelectionChanged: (String, IntRange) -> Unit,
|
||||
clicked: (ListenerType) -> Unit
|
||||
) {
|
||||
check(item is BlockView.Toggle) { "Expected a toggle block, but was: $item" }
|
||||
super.processChangePayload(payloads, item, onTextChanged, onSelectionChanged)
|
||||
super.processChangePayload(payloads, item, onTextChanged, onSelectionChanged, clicked)
|
||||
payloads.forEach { payload ->
|
||||
if (payload.changes.contains(TOGGLE_EMPTY_STATE_CHANGED))
|
||||
placeholder.isVisible = item.isEmpty
|
||||
|
|
|
@ -2,6 +2,6 @@ package com.agileburo.anytype.core_ui.features.page
|
|||
|
||||
sealed class MentionEvent {
|
||||
data class MentionSuggestText(val text: CharSequence) : MentionEvent()
|
||||
object MentionSuggestStart : MentionEvent()
|
||||
data class MentionSuggestStart(val cursorCoordinate : Int, val mentionStart: Int) : MentionEvent()
|
||||
object MentionSuggestStop : MentionEvent()
|
||||
}
|
|
@ -7,6 +7,7 @@ import android.view.View
|
|||
import android.view.inputmethod.InputMethodManager.SHOW_IMPLICIT
|
||||
import android.widget.TextView
|
||||
import com.agileburo.anytype.core_ui.common.*
|
||||
import com.agileburo.anytype.core_ui.extensions.cursorYBottomCoordinate
|
||||
import com.agileburo.anytype.core_ui.extensions.preserveSelection
|
||||
import com.agileburo.anytype.core_ui.extensions.range
|
||||
import com.agileburo.anytype.core_ui.menu.AnytypeContextMenuEvent
|
||||
|
@ -71,6 +72,7 @@ interface TextHolder {
|
|||
fun getMentionImageSizeAndPadding(): Pair<Int, Int>
|
||||
|
||||
private fun setSpannableWithMention(markup: Markup, clicked: (ListenerType) -> Unit) {
|
||||
content.dismissMentionWatchers()
|
||||
content.movementMethod = LinkMovementMethod.getInstance()
|
||||
with(content) {
|
||||
val sizes = getMentionImageSizeAndPadding()
|
||||
|
@ -161,7 +163,12 @@ interface TextHolder {
|
|||
}
|
||||
|
||||
fun setCursor(item: BlockView.Cursor) {
|
||||
item.cursor?.let { content.setSelection(it) }
|
||||
item.cursor?.let {
|
||||
val length = content.text?.length ?: 0
|
||||
if (it in 0..length) {
|
||||
content.setSelection(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setAlignment(alignment: Alignment) {
|
||||
|
@ -181,7 +188,8 @@ interface TextHolder {
|
|||
|
||||
fun setupTextWatcher(
|
||||
onTextChanged: (String, Editable) -> Unit,
|
||||
item: BlockView
|
||||
item: BlockView,
|
||||
onMentionEvent: ((MentionEvent) -> Unit)? = null
|
||||
) {
|
||||
content.addTextChangedListener(
|
||||
DefaultTextWatcher { text ->
|
||||
|
@ -189,8 +197,22 @@ interface TextHolder {
|
|||
}
|
||||
)
|
||||
content.addTextChangedListener(
|
||||
MentionTextWatcher{
|
||||
Timber.d("get mention event : $it")
|
||||
MentionTextWatcher{state ->
|
||||
when (state) {
|
||||
is MentionTextWatcher.MentionTextWatcherState.Start -> {
|
||||
onMentionEvent?.invoke(MentionEvent.MentionSuggestStart(
|
||||
cursorCoordinate = content.cursorYBottomCoordinate(),
|
||||
mentionStart = state.start
|
||||
))
|
||||
}
|
||||
MentionTextWatcher.MentionTextWatcherState.Stop -> {
|
||||
onMentionEvent?.invoke(MentionEvent.MentionSuggestStop)
|
||||
}
|
||||
|
||||
is MentionTextWatcher.MentionTextWatcherState.Text -> {
|
||||
onMentionEvent?.invoke(MentionEvent.MentionSuggestText(state.text))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
@ -215,7 +237,8 @@ interface TextHolder {
|
|||
payloads: List<BlockViewDiffUtil.Payload>,
|
||||
item: BlockView,
|
||||
onTextChanged: (String, Editable) -> Unit,
|
||||
onSelectionChanged: (String, IntRange) -> Unit
|
||||
onSelectionChanged: (String, IntRange) -> Unit,
|
||||
clicked: (ListenerType) -> Unit
|
||||
) = payloads.forEach { payload ->
|
||||
|
||||
Timber.d("Processing $payload for new view:\n$item")
|
||||
|
@ -224,10 +247,19 @@ interface TextHolder {
|
|||
|
||||
if (payload.textChanged()) {
|
||||
content.pauseTextWatchers {
|
||||
if (item is Markup)
|
||||
content.setText(item.toSpannable(), TextView.BufferType.SPANNABLE)
|
||||
else
|
||||
content.setText(item.text)
|
||||
|
||||
when (item) {
|
||||
is BlockView.Paragraph -> {
|
||||
setBlockText(text = item.text, markup = item, clicked = clicked)
|
||||
}
|
||||
else -> {
|
||||
if (item is Markup)
|
||||
content.setText(item.toSpannable(), TextView.BufferType.SPANNABLE)
|
||||
else
|
||||
content.setText(item.text)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
} else if (payload.markupChanged()) {
|
||||
if (item is Markup) setMarkup(item)
|
||||
|
|
|
@ -4,6 +4,7 @@ import com.agileburo.anytype.core_ui.common.Alignment
|
|||
import com.agileburo.anytype.core_ui.common.Markup
|
||||
import com.agileburo.anytype.core_ui.features.page.styling.StylingMode
|
||||
import com.agileburo.anytype.core_ui.features.page.styling.StylingType
|
||||
import com.agileburo.anytype.core_ui.widgets.toolbar.adapter.Mention
|
||||
|
||||
/**
|
||||
* Control panels are UI-elements that allow user to interact with blocks on a page.
|
||||
|
@ -16,7 +17,8 @@ data class ControlPanelState(
|
|||
val focus: Focus? = null,
|
||||
val mainToolbar: Toolbar.Main,
|
||||
val stylingToolbar: Toolbar.Styling,
|
||||
val multiSelect: Toolbar.MultiSelect
|
||||
val multiSelect: Toolbar.MultiSelect,
|
||||
val mentionToolbar: Toolbar.MentionToolbar
|
||||
) {
|
||||
|
||||
sealed class Toolbar {
|
||||
|
@ -103,6 +105,23 @@ data class ControlPanelState(
|
|||
val isScrollAndMoveEnabled: Boolean = false,
|
||||
val count: Int = 0
|
||||
) : Toolbar()
|
||||
|
||||
/**
|
||||
* Toolbar with list of mentions and add new page item.
|
||||
* @property isVisible defines whether the toolbar is visible or not
|
||||
* @property mentionFrom first position of the mentionFilter in text
|
||||
* @property mentionFilter sequence of symbol @ and characters, using for filtering mentions
|
||||
* @property cursorCoordinate y coordinate bottom of the cursor, using for define top border of the toolbar
|
||||
* @property mentions list of all mentions
|
||||
*/
|
||||
data class MentionToolbar(
|
||||
override val isVisible: Boolean,
|
||||
val mentionFrom: Int?,
|
||||
val mentionFilter: String?,
|
||||
val cursorCoordinate: Int?,
|
||||
val updateList: Boolean = false,
|
||||
val mentions: List<Mention> = emptyList()
|
||||
) : Toolbar()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -136,6 +155,14 @@ data class ControlPanelState(
|
|||
type = null,
|
||||
mode = null
|
||||
),
|
||||
mentionToolbar = Toolbar.MentionToolbar(
|
||||
isVisible = false,
|
||||
cursorCoordinate = null,
|
||||
mentionFilter = null,
|
||||
updateList = false,
|
||||
mentionFrom = null,
|
||||
mentions = emptyList()
|
||||
),
|
||||
focus = null
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
package com.agileburo.anytype.core_ui.tools
|
||||
|
||||
import android.graphics.Point
|
||||
import android.graphics.Rect
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
class MentionFooterItemDecorator(private val screen: Point) : RecyclerView.ItemDecoration() {
|
||||
|
||||
override fun getItemOffsets(
|
||||
outRect: Rect,
|
||||
view: View,
|
||||
parent: RecyclerView,
|
||||
state: RecyclerView.State
|
||||
) {
|
||||
parent.adapter?.itemCount?.let { size ->
|
||||
when (parent.getChildAdapterPosition(view)) {
|
||||
size - 1 -> {
|
||||
outRect.bottom = screen.y / 2
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -28,18 +28,11 @@ object MentionHelper {
|
|||
mentionPosition != NO_MENTION_POSITION && start <= mentionPosition && after < count
|
||||
|
||||
/**
|
||||
* return subsequence from [startIndex] to [predicate] or end of sequence with limit [takeNumber]
|
||||
* return subsequence from [startIndex] to end of sequence with limit [takeNumber]
|
||||
*/
|
||||
fun getSubSequenceBeforePredicate(
|
||||
fun getSubSequenceFromStartWithLimit(
|
||||
s: CharSequence,
|
||||
predicate: Char,
|
||||
startIndex: Int,
|
||||
takeNumber: Int
|
||||
): CharSequence =
|
||||
s.indexOf(predicate, startIndex = startIndex).let { pos ->
|
||||
if (pos != -1)
|
||||
s.subSequence(startIndex = startIndex, endIndex = pos).take(takeNumber)
|
||||
else
|
||||
s.subSequence(startIndex = startIndex, endIndex = s.length).take(takeNumber)
|
||||
}
|
||||
): CharSequence = s.subSequence(startIndex = startIndex, endIndex = s.length).take(takeNumber)
|
||||
}
|
|
@ -3,7 +3,7 @@ package com.agileburo.anytype.core_ui.tools
|
|||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import com.agileburo.anytype.core_ui.features.page.MentionEvent
|
||||
import com.agileburo.anytype.core_ui.tools.MentionHelper.getSubSequenceBeforePredicate
|
||||
import com.agileburo.anytype.core_ui.tools.MentionHelper.getSubSequenceFromStartWithLimit
|
||||
import com.agileburo.anytype.core_ui.tools.MentionHelper.isMentionDeleted
|
||||
import com.agileburo.anytype.core_ui.tools.MentionHelper.isMentionSuggestTriggered
|
||||
import timber.log.Timber
|
||||
|
@ -32,7 +32,7 @@ class DefaultTextWatcher(val onTextChanged: (Editable) -> Unit) : TextWatcher {
|
|||
}
|
||||
|
||||
class MentionTextWatcher(
|
||||
private val onMentionEvent: (MentionEvent) -> Unit
|
||||
private val onMentionEvent: (MentionTextWatcherState) -> Unit
|
||||
) : TextWatcher {
|
||||
|
||||
private var mentionCharPosition = NO_MENTION_POSITION
|
||||
|
@ -47,6 +47,11 @@ class MentionTextWatcher(
|
|||
interceptMentionTriggered(text = s, start = start, count = count)
|
||||
}
|
||||
|
||||
fun onDismiss() {
|
||||
Timber.d("Dismiss Mention Watcher $this")
|
||||
mentionCharPosition = NO_MENTION_POSITION
|
||||
}
|
||||
|
||||
/**
|
||||
* If char @ on position [mentionCharPosition] was deleted then send MentionStop event
|
||||
*/
|
||||
|
@ -58,34 +63,40 @@ class MentionTextWatcher(
|
|||
mentionPosition = mentionCharPosition
|
||||
)
|
||||
) {
|
||||
Timber.d("interceptMentionDeleted $this")
|
||||
mentionCharPosition = NO_MENTION_POSITION
|
||||
onMentionEvent(MentionEvent.MentionSuggestStop)
|
||||
onMentionEvent(MentionTextWatcherState.Stop)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for new added char @ and start [MentionEvent.MentionSuggestStart] event
|
||||
* Send all text in range [mentionCharPosition]..[first space or text end] with limit [MENTION_SNIPPET_MAX_LENGTH]
|
||||
* Send all text in range [mentionCharPosition]..[text end] with limit [MENTION_SNIPPET_MAX_LENGTH]
|
||||
*/
|
||||
private fun interceptMentionTriggered(text: CharSequence, start: Int, count: Int) {
|
||||
if (isMentionSuggestTriggered(text, start, count)) {
|
||||
Timber.d("interceptMentionStarted text:$text, start:$start")
|
||||
mentionCharPosition = start
|
||||
onMentionEvent(MentionEvent.MentionSuggestStart)
|
||||
onMentionEvent(MentionTextWatcherState.Start(mentionCharPosition))
|
||||
}
|
||||
if (mentionCharPosition != NO_MENTION_POSITION) {
|
||||
onMentionEvent(
|
||||
MentionEvent.MentionSuggestText(
|
||||
getSubSequenceBeforePredicate(
|
||||
s = text,
|
||||
startIndex = mentionCharPosition,
|
||||
takeNumber = MENTION_SNIPPET_MAX_LENGTH,
|
||||
predicate = SPACE_CHAR
|
||||
)
|
||||
)
|
||||
)
|
||||
getSubSequenceFromStartWithLimit(
|
||||
s = text,
|
||||
startIndex = mentionCharPosition,
|
||||
takeNumber = MENTION_SNIPPET_MAX_LENGTH
|
||||
).let { subSequence ->
|
||||
onMentionEvent(MentionTextWatcherState.Text(subSequence))
|
||||
Timber.d("interceptMentionText text:$text, subSequence:$subSequence")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class MentionTextWatcherState {
|
||||
data class Start(val start: Int) : MentionTextWatcherState()
|
||||
object Stop : MentionTextWatcherState()
|
||||
data class Text(val text: CharSequence) : MentionTextWatcherState()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val SPACE_CHAR = ' '
|
||||
const val MENTION_CHAR = '@'
|
||||
|
|
|
@ -8,6 +8,7 @@ import android.graphics.Rect
|
|||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.text.style.DynamicDrawableSpan
|
||||
import com.agileburo.anytype.core_ui.common.Span
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
class MentionSpan constructor(
|
||||
|
@ -15,8 +16,9 @@ class MentionSpan constructor(
|
|||
private var mResourceId: Int = 0,
|
||||
private var bitmap: Bitmap? = null,
|
||||
private var imageSize: Int,
|
||||
private var imagePadding: Int
|
||||
) : DynamicDrawableSpan() {
|
||||
private var imagePadding: Int,
|
||||
val param: String
|
||||
) : DynamicDrawableSpan(), Span {
|
||||
|
||||
private val endPaddingPx = 4
|
||||
private var mDrawable: Drawable? = null
|
||||
|
|
|
@ -14,6 +14,7 @@ import androidx.appcompat.widget.AppCompatEditText
|
|||
import androidx.core.graphics.withTranslation
|
||||
import com.agileburo.anytype.core_ui.tools.ClipboardInterceptor
|
||||
import com.agileburo.anytype.core_ui.tools.DefaultTextWatcher
|
||||
import com.agileburo.anytype.core_ui.tools.MentionTextWatcher
|
||||
import com.agileburo.anytype.core_ui.widgets.text.highlight.HighlightAttributeReader
|
||||
import com.agileburo.anytype.core_ui.widgets.text.highlight.HighlightDrawer
|
||||
import com.agileburo.anytype.core_utils.ext.multilineIme
|
||||
|
@ -99,6 +100,10 @@ class TextInputWidget : AppCompatEditText {
|
|||
watchers.clear()
|
||||
}
|
||||
|
||||
fun dismissMentionWatchers() {
|
||||
watchers.filterIsInstance(MentionTextWatcher::class.java).forEach { it.onDismiss() }
|
||||
}
|
||||
|
||||
fun pauseTextWatchers(block: () -> Unit) = synchronized(this) {
|
||||
lockTextWatchers()
|
||||
block()
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
package com.agileburo.anytype.core_ui.widgets.toolbar
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.agileburo.anytype.core_ui.R
|
||||
import com.agileburo.anytype.core_ui.widgets.toolbar.adapter.Mention
|
||||
import com.agileburo.anytype.core_ui.widgets.toolbar.adapter.MentionAdapter
|
||||
import kotlinx.android.synthetic.main.widget_mention_menu.view.*
|
||||
|
||||
class MentionToolbar @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : ConstraintLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
private var mentionClick: ((Mention, String) -> Unit)? = null
|
||||
private var newPageClick: (() -> Unit)? = null
|
||||
|
||||
init {
|
||||
LayoutInflater
|
||||
.from(context)
|
||||
.inflate(R.layout.widget_mention_menu, this)
|
||||
setup(context)
|
||||
}
|
||||
|
||||
fun setupClicks(mentionClick: (Mention, String) -> Unit, newPageClick: () -> Unit) {
|
||||
this.mentionClick = mentionClick
|
||||
this.newPageClick = newPageClick
|
||||
}
|
||||
|
||||
private fun setup(context: Context) {
|
||||
with(recyclerView) {
|
||||
val lm = LinearLayoutManager(context)
|
||||
layoutManager = lm
|
||||
adapter = MentionAdapter(
|
||||
data = arrayListOf(),
|
||||
clicked = { mention, filter ->
|
||||
mentionClick?.invoke(mention, filter)
|
||||
},
|
||||
newClicked = {
|
||||
newPageClick?.invoke()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun addItems(items: List<Mention>) {
|
||||
(recyclerView.adapter as? MentionAdapter)?.setData(items)
|
||||
}
|
||||
|
||||
fun updateFilter(filter: String) {
|
||||
(recyclerView.adapter as? MentionAdapter)?.updateFilter(filter)
|
||||
}
|
||||
|
||||
fun getMentionSuggesterWidgetMinHeight() = with(context.resources) {
|
||||
getDimensionPixelSize(R.dimen.mention_suggester_item_height) * MIN_VISIBLE_ITEMS +
|
||||
getDimensionPixelSize(R.dimen.mention_list_padding_top) +
|
||||
getDimensionPixelSize(R.dimen.mention_list_padding_bottom) +
|
||||
getDimensionPixelSize(R.dimen.mention_divider_height)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val MIN_VISIBLE_ITEMS = 4
|
||||
}
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
package com.agileburo.anytype.core_ui.widgets.toolbar.adapter
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.agileburo.anytype.core_ui.R
|
||||
import com.agileburo.anytype.emojifier.Emojifier
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import kotlinx.android.synthetic.main.item_mention.view.*
|
||||
|
||||
class MentionAdapter(
|
||||
private var data: ArrayList<Mention>,
|
||||
private var mentionFilter: String = "",
|
||||
private val clicked: (Mention, String) -> Unit,
|
||||
private val newClicked: () -> Unit
|
||||
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
|
||||
|
||||
fun setData(mentions: List<Mention>) {
|
||||
if (mentions.isEmpty()) {
|
||||
data.clear()
|
||||
} else {
|
||||
data.clear()
|
||||
data.addAll(mentions)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateFilter(filter: String) {
|
||||
mentionFilter = filter
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter all mentions by mentionFilter without symbol @
|
||||
*
|
||||
*/
|
||||
private fun getFilteredData(): List<Mention> =
|
||||
data.filterBy(mentionFilter.removePrefix(MENTION_PREFIX))
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
return when (viewType) {
|
||||
TYPE_NEW_PAGE -> NewPageViewHolder(
|
||||
inflater.inflate(
|
||||
R.layout.item_mention_new_page,
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
TYPE_MENTION -> MentionViewHolder(
|
||||
inflater.inflate(
|
||||
R.layout.item_mention,
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
else -> throw RuntimeException("Wrong viewType")
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = getFilteredData().size + 1
|
||||
|
||||
override fun getItemViewType(position: Int): Int = when (position) {
|
||||
POSITION_NEW_PAGE -> TYPE_NEW_PAGE
|
||||
else -> TYPE_MENTION
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||
when (holder.itemViewType) {
|
||||
TYPE_NEW_PAGE -> (holder as NewPageViewHolder).bind(newClicked)
|
||||
TYPE_MENTION -> (holder as MentionViewHolder).bind(
|
||||
getFilteredData()[position - 1],
|
||||
clicked,
|
||||
mentionFilter
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class NewPageViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
|
||||
fun bind(newClicked: () -> Unit) {
|
||||
itemView.setOnClickListener { newClicked() }
|
||||
}
|
||||
}
|
||||
|
||||
class MentionViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
|
||||
private val tvTitle: TextView = itemView.text
|
||||
private val image: ImageView = itemView.image
|
||||
|
||||
fun bind(item: Mention, clicked: (Mention, String) -> Unit, filter: String) {
|
||||
itemView.setOnClickListener { clicked(item, filter) }
|
||||
tvTitle.text = item.title
|
||||
when {
|
||||
!item.emoji.isNullOrBlank() -> {
|
||||
Glide
|
||||
.with(image)
|
||||
.load(Emojifier.uri(item.emoji))
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.into(image)
|
||||
}
|
||||
!item.image.isNullOrBlank() -> {
|
||||
Glide
|
||||
.with(image)
|
||||
.load(item.image)
|
||||
.centerInside()
|
||||
.circleCrop()
|
||||
.into(image)
|
||||
}
|
||||
else -> {
|
||||
image.setImageResource(R.drawable.ic_block_empty_page)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val MENTION_PREFIX = "@"
|
||||
const val POSITION_NEW_PAGE = 0
|
||||
const val TYPE_NEW_PAGE = 1
|
||||
const val TYPE_MENTION = 2
|
||||
}
|
||||
}
|
||||
|
||||
data class Mention(
|
||||
val id: String,
|
||||
val title: String,
|
||||
val emoji: String?,
|
||||
val image: String?
|
||||
)
|
||||
|
||||
fun List<Mention>.filterBy(text: String): List<Mention> =
|
||||
if (text.isNotEmpty()) filter { it.isContainsText(text) } else this
|
||||
|
||||
fun Mention.isContainsText(text: String): Boolean = title.contains(text, true)
|
17
core-ui/src/main/res/drawable/ic_add_block_below.xml
Normal file
17
core-ui/src/main/res/drawable/ic_add_block_below.xml
Normal file
|
@ -0,0 +1,17 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M12,12m-9.25,-0a9.25,9.25 0,1 0,18.5 -0a9.25,9.25 0,1 0,-18.5 -0"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ACA996"/>
|
||||
<path
|
||||
android:pathData="M7.75,12.75L16.25,12.75A0.75,0.75 0,0 0,17 12L17,12A0.75,0.75 0,0 0,16.25 11.25L7.75,11.25A0.75,0.75 0,0 0,7 12L7,12A0.75,0.75 0,0 0,7.75 12.75z"
|
||||
android:fillColor="#ACA996"/>
|
||||
<path
|
||||
android:pathData="M11.25,7.75L11.25,16.25A0.75,0.75 0,0 0,12 17L12,17A0.75,0.75 0,0 0,12.75 16.25L12.75,7.75A0.75,0.75 0,0 0,12 7L12,7A0.75,0.75 0,0 0,11.25 7.75z"
|
||||
android:fillColor="#ACA996"/>
|
||||
</vector>
|
14
core-ui/src/main/res/drawable/ic_new_page.xml
Normal file
14
core-ui/src/main/res/drawable/ic_new_page.xml
Normal file
|
@ -0,0 +1,14 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="25dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="25">
|
||||
<path
|
||||
android:pathData="M13.5,2.625H12V8.125C12,9.2296 12.8954,10.125 14,10.125H19.5V8.625H14C13.7239,8.625 13.5,8.4011 13.5,8.125V2.625Z"
|
||||
android:fillColor="#ACA996"/>
|
||||
<path
|
||||
android:pathData="M4.75,4.125C4.75,3.4346 5.3096,2.875 6,2.875H13.5858C13.6521,2.875 13.7157,2.9013 13.7626,2.9482L14.2894,2.4214L13.7626,2.9482L19.1768,8.3624C19.2237,8.4093 19.25,8.4729 19.25,8.5392V20.125C19.25,20.8154 18.6904,21.375 18,21.375H6C5.3096,21.375 4.75,20.8154 4.75,20.125V4.125Z"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ACA996"/>
|
||||
</vector>
|
37
core-ui/src/main/res/layout/item_mention.xml
Normal file
37
core-ui/src/main/res/layout/item_mention.xml
Normal file
|
@ -0,0 +1,37 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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="match_parent"
|
||||
android:layout_height="@dimen/mention_suggester_item_height">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginStart="12dp"
|
||||
android:contentDescription="@string/content_desc_mention_suggester_icon"
|
||||
android:scaleType="center"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_add_block_below" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:ellipsize="end"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:maxLines="1"
|
||||
android:textColor="#2C2B27"
|
||||
android:textSize="15sp"
|
||||
tools:text="Is your feature request related to a problem? Please describe."
|
||||
app:layout_constraintBottom_toBottomOf="@+id/image"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/image"
|
||||
app:layout_constraintTop_toTopOf="@+id/image" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
38
core-ui/src/main/res/layout/item_mention_new_page.xml
Normal file
38
core-ui/src/main/res/layout/item_mention_new_page.xml
Normal file
|
@ -0,0 +1,38 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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="match_parent"
|
||||
android:layout_height="@dimen/mention_suggester_item_height">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginStart="12dp"
|
||||
android:contentDescription="@string/content_desc_mention_suggester_icon"
|
||||
android:scaleType="center"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_add_block_below" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:ellipsize="end"
|
||||
android:fontFamily="@font/inter_regular"
|
||||
android:maxLines="1"
|
||||
android:text="@string/mention_suggester_new_page"
|
||||
android:textColor="#2C2B27"
|
||||
android:textSize="15sp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/image"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/image"
|
||||
app:layout_constraintTop_toTopOf="@+id/image"
|
||||
tools:text="Create new page" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
37
core-ui/src/main/res/layout/widget_mention_menu.xml
Normal file
37
core-ui/src/main/res/layout/widget_mention_menu.xml
Normal file
|
@ -0,0 +1,37 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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/container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/white"
|
||||
android:clickable="true"
|
||||
android:focusable="true">
|
||||
|
||||
<View
|
||||
android:id="@+id/divider"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:background="#DFDDD0"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/divider"
|
||||
tools:itemCount="3"
|
||||
tools:listitem="@layout/item_mention" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -118,4 +118,10 @@
|
|||
<dimen name="mention_span_image_padding_header_two">5dp</dimen>
|
||||
<dimen name="mention_span_image_padding_header_one">6dp</dimen>
|
||||
|
||||
<dimen name="mention_suggester_item_height">40dp</dimen>
|
||||
<dimen name="mention_list_padding_top">4dp</dimen>
|
||||
<dimen name="mention_list_padding_bottom">5dp</dimen>
|
||||
<dimen name="mention_suggester_padding_top">10dp</dimen>
|
||||
<dimen name="mention_divider_height">1dp</dimen>
|
||||
|
||||
</resources>
|
|
@ -215,6 +215,8 @@
|
|||
<string name="content_desc_page_toolbar_search">Search pages</string>
|
||||
<string name="content_desc_page_toolbar_navigation">Navigation through pages</string>
|
||||
<string name="content_desc_page_toolbar_add_doc">Add new page</string>
|
||||
<string name="content_desc_mention_suggester_icon">Mention suggester icon</string>
|
||||
<string name="move">Move</string>
|
||||
<string name="mention_suggester_new_page">Create new page</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -197,7 +197,8 @@ class BlockAdapterTest {
|
|||
)
|
||||
),
|
||||
onSelectionChanged = { _, _ -> },
|
||||
onTextChanged = { _, _ -> }
|
||||
onTextChanged = { _, _ -> },
|
||||
clicked = {}
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
|
@ -249,7 +250,8 @@ class BlockAdapterTest {
|
|||
)
|
||||
),
|
||||
onSelectionChanged = { _, _ -> },
|
||||
onTextChanged = { _, _ -> }
|
||||
onTextChanged = { _, _ -> },
|
||||
clicked = {}
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
|
@ -302,7 +304,8 @@ class BlockAdapterTest {
|
|||
)
|
||||
),
|
||||
onSelectionChanged = { _, _ -> },
|
||||
onTextChanged = { _, _ -> }
|
||||
onTextChanged = { _, _ -> },
|
||||
clicked = {}
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
|
@ -365,7 +368,8 @@ class BlockAdapterTest {
|
|||
)
|
||||
),
|
||||
onSelectionChanged = { _, _ -> },
|
||||
onTextChanged = { _, _ -> }
|
||||
onTextChanged = { _, _ -> },
|
||||
clicked = {}
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
|
@ -532,7 +536,8 @@ class BlockAdapterTest {
|
|||
),
|
||||
item = focused,
|
||||
onSelectionChanged = { _, _ -> },
|
||||
onTextChanged = { _, _ -> }
|
||||
onTextChanged = { _, _ -> },
|
||||
clicked = {}
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
|
@ -632,7 +637,8 @@ class BlockAdapterTest {
|
|||
),
|
||||
item = updated,
|
||||
onSelectionChanged = { _, _ -> },
|
||||
onTextChanged = { _, _ -> }
|
||||
onTextChanged = { _, _ -> },
|
||||
clicked = {}
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
|
@ -1527,7 +1533,8 @@ class BlockAdapterTest {
|
|||
)
|
||||
),
|
||||
onSelectionChanged = { _, _ -> },
|
||||
onTextChanged = { _, _ -> }
|
||||
onTextChanged = { _, _ -> },
|
||||
clicked = {}
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
|
@ -1580,7 +1587,8 @@ class BlockAdapterTest {
|
|||
)
|
||||
),
|
||||
onSelectionChanged = { _, _ -> },
|
||||
onTextChanged = { _, _ -> }
|
||||
onTextChanged = { _, _ -> },
|
||||
clicked = {}
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
|
@ -1843,7 +1851,8 @@ class BlockAdapterTest {
|
|||
)
|
||||
),
|
||||
onSelectionChanged = { _, _ -> },
|
||||
onTextChanged = { _, _ -> }
|
||||
onTextChanged = { _, _ -> },
|
||||
clicked = {}
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
|
@ -1989,7 +1998,8 @@ class BlockAdapterTest {
|
|||
)
|
||||
),
|
||||
onSelectionChanged = { _, _ -> },
|
||||
onTextChanged = { _, _ -> }
|
||||
onTextChanged = { _, _ -> },
|
||||
clicked = {}
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
|
@ -2352,7 +2362,8 @@ class BlockAdapterTest {
|
|||
)
|
||||
),
|
||||
onSelectionChanged = { _, _ -> },
|
||||
onTextChanged = { _, _ -> }
|
||||
onTextChanged = { _, _ -> },
|
||||
clicked = {}
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
|
@ -2385,7 +2396,8 @@ class BlockAdapterTest {
|
|||
)
|
||||
),
|
||||
onSelectionChanged = { _, _ -> },
|
||||
onTextChanged = { _, _ -> }
|
||||
onTextChanged = { _, _ -> },
|
||||
clicked = {}
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
|
@ -2418,7 +2430,8 @@ class BlockAdapterTest {
|
|||
)
|
||||
),
|
||||
onSelectionChanged = { _, _ -> },
|
||||
onTextChanged = { _, _ -> }
|
||||
onTextChanged = { _, _ -> },
|
||||
clicked = {}
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
|
@ -2521,7 +2534,8 @@ class BlockAdapterTest {
|
|||
)
|
||||
),
|
||||
onSelectionChanged = { _, _ -> },
|
||||
onTextChanged = { _, _ -> }
|
||||
onTextChanged = { _, _ -> },
|
||||
clicked = {}
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
|
@ -2621,7 +2635,8 @@ class BlockAdapterTest {
|
|||
)
|
||||
),
|
||||
onSelectionChanged = { _, _ -> },
|
||||
onTextChanged = { _, _ -> }
|
||||
onTextChanged = { _, _ -> },
|
||||
clicked = {}
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
|
@ -2767,7 +2782,8 @@ class BlockAdapterTest {
|
|||
)
|
||||
),
|
||||
onSelectionChanged = { _, _ -> },
|
||||
onTextChanged = { _, _ -> }
|
||||
onTextChanged = { _, _ -> },
|
||||
clicked = {}
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
|
@ -2913,7 +2929,8 @@ class BlockAdapterTest {
|
|||
)
|
||||
),
|
||||
onSelectionChanged = { _, _ -> },
|
||||
onTextChanged = { _, _ -> }
|
||||
onTextChanged = { _, _ -> },
|
||||
clicked = {}
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
|
@ -3062,7 +3079,8 @@ class BlockAdapterTest {
|
|||
)
|
||||
),
|
||||
onSelectionChanged = { _, _ -> },
|
||||
onTextChanged = { _, _ -> }
|
||||
onTextChanged = { _, _ -> },
|
||||
clicked = {}
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
|
@ -3216,7 +3234,8 @@ class BlockAdapterTest {
|
|||
)
|
||||
),
|
||||
onSelectionChanged = { _, _ -> },
|
||||
onTextChanged = { _, _ -> }
|
||||
onTextChanged = { _, _ -> },
|
||||
clicked = {}
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
|
@ -3455,7 +3474,8 @@ class BlockAdapterTest {
|
|||
onMarkupActionClicked = { _, _ -> },
|
||||
onTitleTextInputClicked = {},
|
||||
onClickListener = {},
|
||||
clipboardInterceptor = clipboardInterceptor
|
||||
clipboardInterceptor = clipboardInterceptor,
|
||||
onMentionEvent = {}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -224,7 +224,8 @@ class HeaderBlockTest {
|
|||
),
|
||||
item = updated,
|
||||
onSelectionChanged = { _, _ -> },
|
||||
onTextChanged = { _, _ -> }
|
||||
onTextChanged = { _, _ -> },
|
||||
clicked = {}
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
|
@ -279,7 +280,8 @@ class HeaderBlockTest {
|
|||
),
|
||||
item = updated,
|
||||
onSelectionChanged = { _, _ -> },
|
||||
onTextChanged = { _, _ -> }
|
||||
onTextChanged = { _, _ -> },
|
||||
clicked = {}
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
|
@ -334,7 +336,8 @@ class HeaderBlockTest {
|
|||
),
|
||||
item = updated,
|
||||
onSelectionChanged = { _, _ -> },
|
||||
onTextChanged = { _, _ -> }
|
||||
onTextChanged = { _, _ -> },
|
||||
clicked = {}
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
|
@ -372,7 +375,8 @@ class HeaderBlockTest {
|
|||
onMarkupActionClicked = { _, _ -> },
|
||||
onTitleTextInputClicked = {},
|
||||
onClickListener = {},
|
||||
clipboardInterceptor = clipboardInterceptor
|
||||
clipboardInterceptor = clipboardInterceptor,
|
||||
onMentionEvent = {}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -114,7 +114,8 @@ class HighlightingBlockTest {
|
|||
onMarkupActionClicked = { _, _ -> },
|
||||
onTitleTextInputClicked = {},
|
||||
onClickListener = {},
|
||||
clipboardInterceptor = clipboardInterceptor
|
||||
clipboardInterceptor = clipboardInterceptor,
|
||||
onMentionEvent = {}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -378,7 +378,8 @@ class BlockAdapterCursorBindingTest {
|
|||
onMarkupActionClicked = { _, _ -> },
|
||||
onTitleTextInputClicked = {},
|
||||
onClickListener = {},
|
||||
clipboardInterceptor = clipboardInterceptor
|
||||
clipboardInterceptor = clipboardInterceptor,
|
||||
onMentionEvent = {}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
package com.agileburo.anytype.core_ui.tools
|
||||
|
||||
import com.agileburo.anytype.core_ui.tools.MentionHelper.getSubSequenceBeforePredicate
|
||||
import com.agileburo.anytype.core_ui.tools.MentionHelper.getSubSequenceFromStartWithLimit
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
|
@ -8,22 +8,19 @@ import kotlin.test.assertTrue
|
|||
|
||||
class MentionHelperTest {
|
||||
|
||||
private val PREDICATE_CHAR = ' '
|
||||
|
||||
@Test
|
||||
fun `should find mention in the middle of the block`() {
|
||||
|
||||
val text = "Before @mention after"
|
||||
val startIndex = 7
|
||||
|
||||
val result = getSubSequenceBeforePredicate(
|
||||
val result = getSubSequenceFromStartWithLimit(
|
||||
s = text,
|
||||
startIndex = startIndex,
|
||||
takeNumber = 20,
|
||||
predicate = PREDICATE_CHAR
|
||||
takeNumber = 20
|
||||
)
|
||||
|
||||
val expected = "@mention"
|
||||
val expected = "@mention after"
|
||||
|
||||
assertEquals(expected = expected, actual = result)
|
||||
}
|
||||
|
@ -34,11 +31,10 @@ class MentionHelperTest {
|
|||
val text = "Before @mentionAndBiggerThenEleven after"
|
||||
val startIndex = 7
|
||||
|
||||
val result = getSubSequenceBeforePredicate(
|
||||
val result = getSubSequenceFromStartWithLimit(
|
||||
s = text,
|
||||
startIndex = startIndex,
|
||||
takeNumber = 11,
|
||||
predicate = PREDICATE_CHAR
|
||||
takeNumber = 11
|
||||
)
|
||||
|
||||
val expected = "@mentionAnd"
|
||||
|
@ -52,14 +48,13 @@ class MentionHelperTest {
|
|||
val text = "@mention after"
|
||||
val startIndex = 0
|
||||
|
||||
val result = getSubSequenceBeforePredicate(
|
||||
val result = getSubSequenceFromStartWithLimit(
|
||||
s = text,
|
||||
startIndex = startIndex,
|
||||
takeNumber = 20,
|
||||
predicate = PREDICATE_CHAR
|
||||
takeNumber = 20
|
||||
)
|
||||
|
||||
val expected = "@mention"
|
||||
val expected = "@mention after"
|
||||
|
||||
assertEquals(expected = expected, actual = result)
|
||||
}
|
||||
|
@ -70,11 +65,10 @@ class MentionHelperTest {
|
|||
val text = "Before @mentionRealBigWord"
|
||||
val startIndex = 7
|
||||
|
||||
val result = getSubSequenceBeforePredicate(
|
||||
val result = getSubSequenceFromStartWithLimit(
|
||||
s = text,
|
||||
startIndex = startIndex,
|
||||
takeNumber = 11,
|
||||
predicate = PREDICATE_CHAR
|
||||
takeNumber = 11
|
||||
)
|
||||
|
||||
val expected = "@mentionRea"
|
||||
|
@ -88,11 +82,10 @@ class MentionHelperTest {
|
|||
val text = ""
|
||||
val startIndex = 0
|
||||
|
||||
val result = getSubSequenceBeforePredicate(
|
||||
val result = getSubSequenceFromStartWithLimit(
|
||||
s = text,
|
||||
startIndex = startIndex,
|
||||
takeNumber = 11,
|
||||
predicate = PREDICATE_CHAR
|
||||
takeNumber = 11
|
||||
)
|
||||
|
||||
val expected = ""
|
||||
|
@ -106,11 +99,10 @@ class MentionHelperTest {
|
|||
val text = "@"
|
||||
val startIndex = 0
|
||||
|
||||
val result = getSubSequenceBeforePredicate(
|
||||
val result = getSubSequenceFromStartWithLimit(
|
||||
s = text,
|
||||
startIndex = startIndex,
|
||||
takeNumber = 11,
|
||||
predicate = PREDICATE_CHAR
|
||||
takeNumber = 11
|
||||
)
|
||||
|
||||
val expected = "@"
|
||||
|
@ -118,35 +110,16 @@ class MentionHelperTest {
|
|||
assertEquals(expected = expected, actual = result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should find first word in text`() {
|
||||
|
||||
val text = "text without mention"
|
||||
val startIndex = 0
|
||||
|
||||
val result = getSubSequenceBeforePredicate(
|
||||
s = text,
|
||||
startIndex = startIndex,
|
||||
takeNumber = 11,
|
||||
predicate = PREDICATE_CHAR
|
||||
)
|
||||
|
||||
val expected = "text"
|
||||
|
||||
assertEquals(expected = expected, actual = result)
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun `should throw error when take number is negative`() {
|
||||
|
||||
val text = "text without mention"
|
||||
val startIndex = 0
|
||||
|
||||
val result = getSubSequenceBeforePredicate(
|
||||
val result = getSubSequenceFromStartWithLimit(
|
||||
s = text,
|
||||
startIndex = startIndex,
|
||||
takeNumber = -11,
|
||||
predicate = PREDICATE_CHAR
|
||||
takeNumber = -11
|
||||
)
|
||||
|
||||
val expected = "text"
|
||||
|
@ -160,11 +133,10 @@ class MentionHelperTest {
|
|||
val text = "text without mention"
|
||||
val startIndex = 100
|
||||
|
||||
val result = getSubSequenceBeforePredicate(
|
||||
val result = getSubSequenceFromStartWithLimit(
|
||||
s = text,
|
||||
startIndex = startIndex,
|
||||
takeNumber = 0,
|
||||
predicate = PREDICATE_CHAR
|
||||
takeNumber = 0
|
||||
)
|
||||
|
||||
val expected = "text"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package com.agileburo.anytype.core_utils.ext
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
|
@ -197,4 +198,12 @@ fun Fragment.screen(): Point {
|
|||
val p = Point()
|
||||
display.getSize(p)
|
||||
return p
|
||||
}
|
||||
|
||||
fun Activity.screen(): Point {
|
||||
val wm = (getSystemService(Context.WINDOW_SERVICE) as WindowManager)
|
||||
val display = wm.defaultDisplay
|
||||
val p = Point()
|
||||
display.getSize(p)
|
||||
return p
|
||||
}
|
|
@ -4,7 +4,7 @@ import com.agileburo.anytype.domain.base.BaseUseCase
|
|||
import com.agileburo.anytype.domain.base.Either
|
||||
import com.agileburo.anytype.domain.block.repo.BlockRepository
|
||||
|
||||
class GetListPages(private val repo: BlockRepository) :
|
||||
open class GetListPages(private val repo: BlockRepository) :
|
||||
BaseUseCase<GetListPages.Response, Unit>() {
|
||||
|
||||
override suspend fun run(params: Unit): Either<Throwable, Response> = safe {
|
||||
|
|
|
@ -122,6 +122,7 @@ fun BlockEntity.Content.Text.Mark.mark(): Mark = when (type) {
|
|||
Mark.newBuilder()
|
||||
.setType(Block.Content.Text.Mark.Type.Mention)
|
||||
.setRange(range.range())
|
||||
.setParam(param)
|
||||
.build()
|
||||
}
|
||||
else -> throw IllegalStateException("Unsupported mark type: ${type.name}")
|
||||
|
|
|
@ -5,6 +5,7 @@ import com.agileburo.anytype.core_ui.common.Markup
|
|||
import com.agileburo.anytype.core_ui.features.navigation.PageLinkView
|
||||
import com.agileburo.anytype.core_ui.features.page.BlockView
|
||||
import com.agileburo.anytype.core_ui.model.UiBlock
|
||||
import com.agileburo.anytype.core_ui.widgets.toolbar.adapter.Mention
|
||||
import com.agileburo.anytype.domain.block.model.Block
|
||||
import com.agileburo.anytype.domain.config.DebugSettings
|
||||
import com.agileburo.anytype.domain.dashboard.model.HomeDashboard
|
||||
|
@ -254,4 +255,16 @@ fun Block.Fields.toImageView(urlBuilder: UrlBuilder): String? = this.iconImage.l
|
|||
|
||||
fun Block.Fields.toEmojiView(): String? = this.iconEmoji.let { emoji ->
|
||||
if (emoji.isNullOrBlank()) null else emoji
|
||||
}
|
||||
}
|
||||
|
||||
fun PageInfo.toMentionView() = Mention(
|
||||
id = id,
|
||||
title = fields.getName(),
|
||||
image = fields.iconImage,
|
||||
emoji = fields.iconEmoji
|
||||
)
|
||||
|
||||
fun Block.Fields.getName(): String =
|
||||
this.name.let { name ->
|
||||
if (name.isNullOrBlank()) "Untitled" else name
|
||||
}
|
|
@ -7,6 +7,7 @@ import com.agileburo.anytype.core_ui.features.page.styling.StylingMode
|
|||
import com.agileburo.anytype.core_ui.features.page.styling.StylingType
|
||||
import com.agileburo.anytype.core_ui.state.ControlPanelState
|
||||
import com.agileburo.anytype.core_ui.state.ControlPanelState.Toolbar
|
||||
import com.agileburo.anytype.core_ui.widgets.toolbar.adapter.Mention
|
||||
import com.agileburo.anytype.domain.block.model.Block
|
||||
import com.agileburo.anytype.domain.ext.content
|
||||
import com.agileburo.anytype.domain.ext.overlap
|
||||
|
@ -151,6 +152,12 @@ sealed class ControlPanelMachine {
|
|||
object OnEnterScrollAndMoveModeClicked : Event()
|
||||
|
||||
data class OnRefresh(val target: Block?) : Event()
|
||||
|
||||
data class OnShowMentionToolbar(val cursorCoordinate: Int, val mentionFrom: Int) : Event()
|
||||
data class OnMentionFilterText(val text: String) : Event()
|
||||
data class OnGetMentionsList(val mentions: List<Mention>) : Event()
|
||||
object OnMentionClicked : Event()
|
||||
object OnHideMentionToolbar : Event()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -163,6 +170,24 @@ sealed class ControlPanelMachine {
|
|||
|
||||
var selection: IntRange? = null
|
||||
|
||||
private fun onSelectionChangedMentionState(
|
||||
state: Toolbar.MentionToolbar,
|
||||
start: Int
|
||||
): Toolbar.MentionToolbar {
|
||||
val from = state.mentionFrom
|
||||
return if (state.isVisible && from != null && start < from) {
|
||||
state.copy(
|
||||
isVisible = false,
|
||||
cursorCoordinate = null,
|
||||
mentionFrom = null,
|
||||
updateList = false,
|
||||
mentionFilter = null,
|
||||
mentions = emptyList())
|
||||
} else {
|
||||
state.copy()
|
||||
}
|
||||
}
|
||||
|
||||
override val function: suspend (ControlPanelState, Event) -> ControlPanelState
|
||||
get() = { state, event ->
|
||||
reduce(
|
||||
|
@ -223,16 +248,29 @@ sealed class ControlPanelMachine {
|
|||
color = color,
|
||||
background = background
|
||||
)
|
||||
),
|
||||
mentionToolbar = onSelectionChangedMentionState(
|
||||
state = state.mentionToolbar,
|
||||
start = event.selection.first
|
||||
)
|
||||
)
|
||||
} else {
|
||||
state.copy()
|
||||
state.copy(
|
||||
mentionToolbar = onSelectionChangedMentionState(
|
||||
state = state.mentionToolbar,
|
||||
start = event.selection.first
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
state.copy(
|
||||
mainToolbar = state.mainToolbar.copy(
|
||||
isVisible = (!state.multiSelect.isVisible && event.selection.first == event.selection.last)
|
||||
),
|
||||
mentionToolbar = onSelectionChangedMentionState(
|
||||
state = state.mentionToolbar,
|
||||
start = event.selection.first
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -602,7 +640,15 @@ sealed class ControlPanelMachine {
|
|||
)
|
||||
is Event.OnFocusChanged -> {
|
||||
when {
|
||||
state.multiSelect.isVisible -> state.copy()
|
||||
state.multiSelect.isVisible -> state.copy(
|
||||
mentionToolbar = state.mentionToolbar.copy(
|
||||
isVisible = false,
|
||||
cursorCoordinate = null,
|
||||
mentionFrom = null,
|
||||
mentionFilter = null,
|
||||
mentions = emptyList()
|
||||
)
|
||||
)
|
||||
!state.mainToolbar.isVisible -> state.copy(
|
||||
mainToolbar = state.mainToolbar.copy(
|
||||
isVisible = true
|
||||
|
@ -615,6 +661,13 @@ sealed class ControlPanelMachine {
|
|||
type = ControlPanelState.Focus.Type.valueOf(
|
||||
value = event.style.name
|
||||
)
|
||||
),
|
||||
mentionToolbar = state.mentionToolbar.copy(
|
||||
isVisible = false,
|
||||
cursorCoordinate = null,
|
||||
mentionFrom = null,
|
||||
mentionFilter = null,
|
||||
mentions = emptyList()
|
||||
)
|
||||
)
|
||||
else -> {
|
||||
|
@ -627,6 +680,13 @@ sealed class ControlPanelMachine {
|
|||
),
|
||||
stylingToolbar = state.stylingToolbar.copy(
|
||||
isVisible = false
|
||||
),
|
||||
mentionToolbar = state.mentionToolbar.copy(
|
||||
isVisible = false,
|
||||
cursorCoordinate = null,
|
||||
mentionFrom = null,
|
||||
mentionFilter = null,
|
||||
mentions = emptyList()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -653,6 +713,47 @@ sealed class ControlPanelMachine {
|
|||
isScrollAndMoveEnabled = false
|
||||
)
|
||||
)
|
||||
is Event.OnShowMentionToolbar -> state.copy(
|
||||
mentionToolbar = state.mentionToolbar.copy(
|
||||
isVisible = true,
|
||||
cursorCoordinate = event.cursorCoordinate,
|
||||
mentionFilter = "",
|
||||
updateList = false,
|
||||
mentionFrom = event.mentionFrom
|
||||
)
|
||||
)
|
||||
is Event.OnHideMentionToolbar -> state.copy(
|
||||
mentionToolbar = state.mentionToolbar.copy(
|
||||
isVisible = false,
|
||||
cursorCoordinate = null,
|
||||
updateList = true,
|
||||
mentionFrom = null,
|
||||
mentionFilter = null,
|
||||
mentions = emptyList()
|
||||
)
|
||||
)
|
||||
is Event.OnGetMentionsList -> state.copy(
|
||||
mentionToolbar = state.mentionToolbar.copy(
|
||||
mentions = event.mentions,
|
||||
updateList = true
|
||||
)
|
||||
)
|
||||
is Event.OnMentionClicked -> state.copy(
|
||||
mentionToolbar = state.mentionToolbar.copy(
|
||||
isVisible = false,
|
||||
cursorCoordinate = null,
|
||||
mentionFrom = null,
|
||||
updateList = true,
|
||||
mentionFilter = null,
|
||||
mentions = emptyList()
|
||||
)
|
||||
)
|
||||
is Event.OnMentionFilterText -> state.copy(
|
||||
mentionToolbar = state.mentionToolbar.copy(
|
||||
mentionFilter = event.text,
|
||||
updateList = false
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fun target(block: Block): Toolbar.Styling.Target {
|
||||
|
|
|
@ -6,16 +6,14 @@ import androidx.lifecycle.viewModelScope
|
|||
import com.agileburo.anytype.core_ui.common.Alignment
|
||||
import com.agileburo.anytype.core_ui.common.Markup
|
||||
import com.agileburo.anytype.core_ui.extensions.updateSelection
|
||||
import com.agileburo.anytype.core_ui.features.page.BlockDimensions
|
||||
import com.agileburo.anytype.core_ui.features.page.BlockView
|
||||
import com.agileburo.anytype.core_ui.features.page.ListenerType
|
||||
import com.agileburo.anytype.core_ui.features.page.TurnIntoActionReceiver
|
||||
import com.agileburo.anytype.core_ui.features.page.*
|
||||
import com.agileburo.anytype.core_ui.features.page.scrollandmove.ScrollAndMoveTargetDescriptor.Companion.END_RANGE
|
||||
import com.agileburo.anytype.core_ui.features.page.scrollandmove.ScrollAndMoveTargetDescriptor.Companion.INNER_RANGE
|
||||
import com.agileburo.anytype.core_ui.features.page.scrollandmove.ScrollAndMoveTargetDescriptor.Companion.START_RANGE
|
||||
import com.agileburo.anytype.core_ui.model.UiBlock
|
||||
import com.agileburo.anytype.core_ui.state.ControlPanelState
|
||||
import com.agileburo.anytype.core_ui.widgets.ActionItemType
|
||||
import com.agileburo.anytype.core_ui.widgets.toolbar.adapter.Mention
|
||||
import com.agileburo.anytype.core_utils.common.EventWrapper
|
||||
import com.agileburo.anytype.core_utils.ext.*
|
||||
import com.agileburo.anytype.core_utils.ui.ViewStateViewModel
|
||||
|
@ -30,14 +28,17 @@ import com.agileburo.anytype.domain.editor.Editor
|
|||
import com.agileburo.anytype.domain.event.interactor.InterceptEvents
|
||||
import com.agileburo.anytype.domain.event.model.Event
|
||||
import com.agileburo.anytype.domain.event.model.Payload
|
||||
import com.agileburo.anytype.domain.ext.addMention
|
||||
import com.agileburo.anytype.domain.ext.asMap
|
||||
import com.agileburo.anytype.domain.ext.content
|
||||
import com.agileburo.anytype.domain.ext.textStyle
|
||||
import com.agileburo.anytype.domain.misc.UrlBuilder
|
||||
import com.agileburo.anytype.domain.page.*
|
||||
import com.agileburo.anytype.domain.page.navigation.GetListPages
|
||||
import com.agileburo.anytype.presentation.common.StateReducer
|
||||
import com.agileburo.anytype.presentation.common.SupportCommand
|
||||
import com.agileburo.anytype.presentation.mapper.style
|
||||
import com.agileburo.anytype.presentation.mapper.toMentionView
|
||||
import com.agileburo.anytype.presentation.navigation.AppNavigation
|
||||
import com.agileburo.anytype.presentation.navigation.SupportNavigation
|
||||
import com.agileburo.anytype.presentation.page.ControlPanelMachine.Interactor
|
||||
|
@ -65,7 +66,8 @@ class PageViewModel(
|
|||
private val reducer: StateReducer<List<Block>, Event>,
|
||||
private val urlBuilder: UrlBuilder,
|
||||
private val renderer: DefaultBlockViewRenderer,
|
||||
private val orchestrator: Orchestrator
|
||||
private val orchestrator: Orchestrator,
|
||||
private val getListPages: GetListPages
|
||||
) : ViewStateViewModel<ViewState>(),
|
||||
SupportNavigation<EventWrapper<AppNavigation.Command>>,
|
||||
SupportCommand<Command>,
|
||||
|
@ -108,6 +110,11 @@ class PageViewModel(
|
|||
*/
|
||||
private var mediaBlockId = ""
|
||||
|
||||
/**
|
||||
* Current position of last mentionFilter or -1 if none
|
||||
*/
|
||||
private var mentionFrom = -1
|
||||
|
||||
override val navigation = MutableLiveData<EventWrapper<AppNavigation.Command>>()
|
||||
override val commands = MutableLiveData<EventWrapper<Command>>()
|
||||
|
||||
|
@ -1837,6 +1844,93 @@ class PageViewModel(
|
|||
navigation.postValue(EventWrapper(AppNavigation.Command.OpenPageSearch))
|
||||
}
|
||||
|
||||
fun onMentionEvent(mentionEvent: MentionEvent) {
|
||||
when (mentionEvent) {
|
||||
is MentionEvent.MentionSuggestText -> {
|
||||
controlPanelInteractor.onEvent(
|
||||
ControlPanelMachine.Event.OnMentionFilterText(
|
||||
text = mentionEvent.text.toString()
|
||||
)
|
||||
)
|
||||
}
|
||||
is MentionEvent.MentionSuggestStart -> {
|
||||
mentionFrom = mentionEvent.mentionStart
|
||||
controlPanelInteractor.onEvent(
|
||||
ControlPanelMachine.Event.OnShowMentionToolbar(
|
||||
cursorCoordinate = mentionEvent.cursorCoordinate,
|
||||
mentionFrom = mentionEvent.mentionStart
|
||||
)
|
||||
)
|
||||
viewModelScope.launch {
|
||||
getListPages.invoke(Unit).proceed(
|
||||
failure = { it.timber() },
|
||||
success = { response ->
|
||||
controlPanelInteractor.onEvent(
|
||||
ControlPanelMachine.Event.OnGetMentionsList(
|
||||
mentions = response.listPages.map { it.toMentionView() }
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
MentionEvent.MentionSuggestStop -> {
|
||||
mentionFrom = -1
|
||||
controlPanelInteractor.onEvent(
|
||||
ControlPanelMachine.Event.OnHideMentionToolbar
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onAddMentionNewPageClicked() {
|
||||
onAddNewPageClicked()
|
||||
}
|
||||
|
||||
fun onMentionSuggestClick(mention: Mention, mentionTrigger: String) {
|
||||
Timber.d("onAddMentionClicked, suggest:$mention, from:$mentionFrom")
|
||||
|
||||
controlPanelInteractor.onEvent(ControlPanelMachine.Event.OnMentionClicked)
|
||||
|
||||
val target = blocks.first { it.id == focus.value }
|
||||
|
||||
val new = target.addMention(
|
||||
mentionText = mention.title,
|
||||
mentionId = mention.id,
|
||||
from = mentionFrom,
|
||||
mentionTrigger = mentionTrigger
|
||||
)
|
||||
|
||||
blocks = blocks.map { block ->
|
||||
if (block.id != target.id)
|
||||
block
|
||||
else
|
||||
new
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
val position = mentionFrom + mention.title.length + 1
|
||||
orchestrator.stores.focus.update(
|
||||
t = Editor.Focus(
|
||||
id = new.id,
|
||||
cursor = Editor.Cursor.Range(IntRange(position, position))
|
||||
)
|
||||
)
|
||||
refresh()
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
proceedWithUpdatingText(
|
||||
intent = Intent.Text.UpdateText(
|
||||
context = context,
|
||||
target = new.id,
|
||||
text = new.content<Content.Text>().text,
|
||||
marks = new.content<Content.Text>().marks
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onMultiSelectModeBlockClicked() {
|
||||
controlPanelInteractor.onEvent(
|
||||
ControlPanelMachine.Event.OnMultiSelectModeBlockClick(
|
||||
|
|
|
@ -4,12 +4,12 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.agileburo.anytype.domain.block.interactor.RemoveLinkMark
|
||||
import com.agileburo.anytype.domain.block.interactor.UpdateLinkMarks
|
||||
import com.agileburo.anytype.domain.block.interactor.UploadBlock
|
||||
import com.agileburo.anytype.domain.block.model.Block
|
||||
import com.agileburo.anytype.domain.event.interactor.InterceptEvents
|
||||
import com.agileburo.anytype.domain.event.model.Event
|
||||
import com.agileburo.anytype.domain.misc.UrlBuilder
|
||||
import com.agileburo.anytype.domain.page.*
|
||||
import com.agileburo.anytype.domain.page.navigation.GetListPages
|
||||
import com.agileburo.anytype.presentation.common.StateReducer
|
||||
import com.agileburo.anytype.presentation.page.editor.Orchestrator
|
||||
import com.agileburo.anytype.presentation.page.render.DefaultBlockViewRenderer
|
||||
|
@ -26,7 +26,8 @@ open class PageViewModelFactory(
|
|||
private val documentEventReducer: StateReducer<List<Block>, Event>,
|
||||
private val urlBuilder: UrlBuilder,
|
||||
private val renderer: DefaultBlockViewRenderer,
|
||||
private val interactor: Orchestrator
|
||||
private val interactor: Orchestrator,
|
||||
private val getListPages: GetListPages
|
||||
) : ViewModelProvider.Factory {
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
|
@ -43,7 +44,8 @@ open class PageViewModelFactory(
|
|||
urlBuilder = urlBuilder,
|
||||
renderer = renderer,
|
||||
createDocument = createDocument,
|
||||
orchestrator = interactor
|
||||
orchestrator = interactor,
|
||||
getListPages = getListPages
|
||||
) as T
|
||||
}
|
||||
}
|
|
@ -50,6 +50,223 @@ class ControlPanelStateReducerTest {
|
|||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `state should hide mentions when cursor before mentions start and widget is visible`() {
|
||||
|
||||
val given = ControlPanelState(
|
||||
focus = ControlPanelState.Focus(
|
||||
id = MockDataFactory.randomUuid(),
|
||||
type = ControlPanelState.Focus.Type.P
|
||||
),
|
||||
mainToolbar = ControlPanelState.Toolbar.Main(
|
||||
isVisible = true
|
||||
),
|
||||
stylingToolbar = ControlPanelState.Toolbar.Styling(
|
||||
isVisible = false,
|
||||
mode = null,
|
||||
type = null
|
||||
),
|
||||
multiSelect = ControlPanelState.Toolbar.MultiSelect(
|
||||
isVisible = false
|
||||
),
|
||||
mentionToolbar = ControlPanelState.Toolbar.MentionToolbar(
|
||||
isVisible = true,
|
||||
cursorCoordinate = 333,
|
||||
mentionFilter = "start",
|
||||
mentionFrom = 10
|
||||
)
|
||||
)
|
||||
|
||||
val event = ControlPanelMachine.Event.OnSelectionChanged(
|
||||
selection = IntRange(9,9)
|
||||
)
|
||||
|
||||
val actual = runBlocking {
|
||||
reducer.reduce(
|
||||
state = given,
|
||||
event = event
|
||||
)
|
||||
}
|
||||
|
||||
val expected = given.copy(
|
||||
mentionToolbar = ControlPanelState.Toolbar.MentionToolbar(
|
||||
isVisible = false,
|
||||
cursorCoordinate = null,
|
||||
mentionFilter = null,
|
||||
mentionFrom = null
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
expected = expected,
|
||||
actual = actual
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `state should not hide mentions when cursor after mention start`() {
|
||||
|
||||
val given = ControlPanelState(
|
||||
focus = ControlPanelState.Focus(
|
||||
id = MockDataFactory.randomUuid(),
|
||||
type = ControlPanelState.Focus.Type.P
|
||||
),
|
||||
mainToolbar = ControlPanelState.Toolbar.Main(
|
||||
isVisible = true
|
||||
),
|
||||
stylingToolbar = ControlPanelState.Toolbar.Styling(
|
||||
isVisible = false,
|
||||
mode = null,
|
||||
type = null
|
||||
),
|
||||
multiSelect = ControlPanelState.Toolbar.MultiSelect(
|
||||
isVisible = false
|
||||
),
|
||||
mentionToolbar = ControlPanelState.Toolbar.MentionToolbar(
|
||||
isVisible = true,
|
||||
cursorCoordinate = 333,
|
||||
mentionFilter = "start",
|
||||
mentionFrom = 10
|
||||
)
|
||||
)
|
||||
|
||||
val event = ControlPanelMachine.Event.OnSelectionChanged(
|
||||
selection = IntRange(11,11)
|
||||
)
|
||||
|
||||
val actual = runBlocking {
|
||||
reducer.reduce(
|
||||
state = given,
|
||||
event = event
|
||||
)
|
||||
}
|
||||
|
||||
val expected = given.copy(
|
||||
mentionToolbar = ControlPanelState.Toolbar.MentionToolbar(
|
||||
isVisible = true,
|
||||
cursorCoordinate = 333,
|
||||
mentionFilter = "start",
|
||||
mentionFrom = 10
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
expected = expected,
|
||||
actual = actual
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `state should hide mentions after focus changed`() {
|
||||
|
||||
val given = ControlPanelState(
|
||||
focus = ControlPanelState.Focus(
|
||||
id = MockDataFactory.randomUuid(),
|
||||
type = ControlPanelState.Focus.Type.P
|
||||
),
|
||||
mainToolbar = ControlPanelState.Toolbar.Main(
|
||||
isVisible = true
|
||||
),
|
||||
stylingToolbar = ControlPanelState.Toolbar.Styling(
|
||||
isVisible = false,
|
||||
mode = null,
|
||||
type = null
|
||||
),
|
||||
multiSelect = ControlPanelState.Toolbar.MultiSelect(
|
||||
isVisible = false
|
||||
),
|
||||
mentionToolbar = ControlPanelState.Toolbar.MentionToolbar(
|
||||
isVisible = true,
|
||||
cursorCoordinate = 333,
|
||||
mentionFilter = "start",
|
||||
mentionFrom = 10
|
||||
)
|
||||
)
|
||||
|
||||
val event = ControlPanelMachine.Event.OnFocusChanged(
|
||||
id = MockDataFactory.randomUuid(),
|
||||
style = Block.Content.Text.Style.P
|
||||
)
|
||||
|
||||
val actual = runBlocking {
|
||||
reducer.reduce(
|
||||
state = given,
|
||||
event = event
|
||||
)
|
||||
}
|
||||
|
||||
val expected = given.copy(
|
||||
focus = ControlPanelState.Focus(
|
||||
id = event.id,
|
||||
type = ControlPanelState.Focus.Type.P
|
||||
),
|
||||
mentionToolbar = ControlPanelState.Toolbar.MentionToolbar(
|
||||
isVisible = false,
|
||||
cursorCoordinate = null,
|
||||
mentionFilter = null,
|
||||
mentionFrom = null
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
expected = expected,
|
||||
actual = actual
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `state should hide mentions after selection chaged`() {
|
||||
|
||||
val given = ControlPanelState(
|
||||
focus = ControlPanelState.Focus(
|
||||
id = MockDataFactory.randomUuid(),
|
||||
type = ControlPanelState.Focus.Type.P
|
||||
),
|
||||
mainToolbar = ControlPanelState.Toolbar.Main(
|
||||
isVisible = true
|
||||
),
|
||||
stylingToolbar = ControlPanelState.Toolbar.Styling(
|
||||
isVisible = false,
|
||||
mode = null,
|
||||
type = null
|
||||
),
|
||||
multiSelect = ControlPanelState.Toolbar.MultiSelect(
|
||||
isVisible = false
|
||||
),
|
||||
mentionToolbar = ControlPanelState.Toolbar.MentionToolbar(
|
||||
isVisible = true,
|
||||
cursorCoordinate = 333,
|
||||
mentionFilter = "start",
|
||||
mentionFrom = 10
|
||||
)
|
||||
)
|
||||
|
||||
val event = ControlPanelMachine.Event.OnSelectionChanged(
|
||||
selection = IntRange(8, 8)
|
||||
)
|
||||
|
||||
val actual = runBlocking {
|
||||
reducer.reduce(
|
||||
state = given,
|
||||
event = event
|
||||
)
|
||||
}
|
||||
|
||||
val expected = given.copy(
|
||||
mentionToolbar = ControlPanelState.Toolbar.MentionToolbar(
|
||||
isVisible = false,
|
||||
cursorCoordinate = null,
|
||||
mentionFilter = null,
|
||||
mentionFrom = null
|
||||
)
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
expected = expected,
|
||||
actual = actual
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `state should have only focus changed`() {
|
||||
|
||||
|
@ -68,6 +285,12 @@ class ControlPanelStateReducerTest {
|
|||
),
|
||||
multiSelect = ControlPanelState.Toolbar.MultiSelect(
|
||||
isVisible = false
|
||||
),
|
||||
mentionToolbar = ControlPanelState.Toolbar.MentionToolbar(
|
||||
isVisible = false,
|
||||
cursorCoordinate = null,
|
||||
mentionFilter = null,
|
||||
mentionFrom = null
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -114,6 +337,12 @@ class ControlPanelStateReducerTest {
|
|||
),
|
||||
multiSelect = ControlPanelState.Toolbar.MultiSelect(
|
||||
isVisible = false
|
||||
),
|
||||
mentionToolbar = ControlPanelState.Toolbar.MentionToolbar(
|
||||
isVisible = false,
|
||||
cursorCoordinate = null,
|
||||
mentionFilter = null,
|
||||
mentionFrom = null
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -152,6 +381,12 @@ class ControlPanelStateReducerTest {
|
|||
),
|
||||
multiSelect = ControlPanelState.Toolbar.MultiSelect(
|
||||
isVisible = false
|
||||
),
|
||||
mentionToolbar = ControlPanelState.Toolbar.MentionToolbar(
|
||||
isVisible = false,
|
||||
cursorCoordinate = null,
|
||||
mentionFilter = null,
|
||||
mentionFrom = null
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -200,6 +435,12 @@ class ControlPanelStateReducerTest {
|
|||
multiSelect = ControlPanelState.Toolbar.MultiSelect(
|
||||
isVisible = false,
|
||||
count = 2
|
||||
),
|
||||
mentionToolbar = ControlPanelState.Toolbar.MentionToolbar(
|
||||
isVisible = false,
|
||||
cursorCoordinate = null,
|
||||
mentionFilter = null,
|
||||
mentionFrom = null
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -219,6 +460,12 @@ class ControlPanelStateReducerTest {
|
|||
multiSelect = ControlPanelState.Toolbar.MultiSelect(
|
||||
isVisible = true,
|
||||
count = 0
|
||||
),
|
||||
mentionToolbar = ControlPanelState.Toolbar.MentionToolbar(
|
||||
isVisible = false,
|
||||
cursorCoordinate = null,
|
||||
mentionFilter = null,
|
||||
mentionFrom = null
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -256,6 +503,12 @@ class ControlPanelStateReducerTest {
|
|||
multiSelect = ControlPanelState.Toolbar.MultiSelect(
|
||||
isVisible = true,
|
||||
count = 0
|
||||
),
|
||||
mentionToolbar = ControlPanelState.Toolbar.MentionToolbar(
|
||||
isVisible = false,
|
||||
cursorCoordinate = null,
|
||||
mentionFilter = null,
|
||||
mentionFrom = null
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -275,6 +528,12 @@ class ControlPanelStateReducerTest {
|
|||
multiSelect = ControlPanelState.Toolbar.MultiSelect(
|
||||
isVisible = true,
|
||||
count = 3
|
||||
),
|
||||
mentionToolbar = ControlPanelState.Toolbar.MentionToolbar(
|
||||
isVisible = false,
|
||||
cursorCoordinate = null,
|
||||
mentionFilter = null,
|
||||
mentionFrom = null
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ import com.agileburo.anytype.domain.ext.content
|
|||
import com.agileburo.anytype.domain.misc.UrlBuilder
|
||||
import com.agileburo.anytype.domain.page.*
|
||||
import com.agileburo.anytype.domain.page.bookmark.SetupBookmark
|
||||
import com.agileburo.anytype.domain.page.navigation.GetListPages
|
||||
import com.agileburo.anytype.presentation.MockBlockFactory
|
||||
import com.agileburo.anytype.presentation.navigation.AppNavigation
|
||||
import com.agileburo.anytype.presentation.page.editor.Command
|
||||
|
@ -100,6 +101,9 @@ class PageViewModelTest {
|
|||
@Mock
|
||||
lateinit var splitBlock: SplitBlock
|
||||
|
||||
@Mock
|
||||
lateinit var getListPages: GetListPages
|
||||
|
||||
@Mock
|
||||
lateinit var createPage: CreatePage
|
||||
|
||||
|
@ -4318,6 +4322,7 @@ class PageViewModelTest {
|
|||
)
|
||||
|
||||
vm = PageViewModel(
|
||||
getListPages = getListPages,
|
||||
openPage = openPage,
|
||||
closePage = closePage,
|
||||
createPage = createPage,
|
||||
|
|
|
@ -0,0 +1,171 @@
|
|||
package com.agileburo.anytype.presentation.page.editor
|
||||
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import com.agileburo.anytype.core_ui.common.Markup
|
||||
import com.agileburo.anytype.core_ui.features.page.BlockView
|
||||
import com.agileburo.anytype.core_ui.features.page.MentionEvent
|
||||
import com.agileburo.anytype.core_ui.widgets.toolbar.adapter.Mention
|
||||
import com.agileburo.anytype.domain.block.model.Block
|
||||
import com.agileburo.anytype.presentation.page.PageViewModel
|
||||
import com.agileburo.anytype.presentation.util.CoroutinesTestRule
|
||||
import com.jraska.livedata.test
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.mockito.MockitoAnnotations
|
||||
|
||||
class EditorMentionTest : EditorPresentationTestSetup() {
|
||||
|
||||
@get:Rule
|
||||
val rule = InstantTaskExecutorRule()
|
||||
|
||||
@get:Rule
|
||||
val coroutineTestRule = CoroutinesTestRule()
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
MockitoAnnotations.initMocks(this)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should update text with cursor position`() {
|
||||
|
||||
val mentionTrigger = "@a"
|
||||
val from = 11
|
||||
val givenText = "page about $mentionTrigger music"
|
||||
val mentionText = "Avant-Garde Jazz"
|
||||
val mentionHash = "ryew78yfhiuwehudc"
|
||||
|
||||
val a = Block(
|
||||
id = "dfhkshfjkhsdjhfjkhsjkd",
|
||||
fields = Block.Fields.empty(),
|
||||
children = emptyList(),
|
||||
content = Block.Content.Text(
|
||||
text = givenText,
|
||||
marks = listOf(
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(
|
||||
start = 0,
|
||||
endInclusive = 3
|
||||
),
|
||||
type = Block.Content.Text.Mark.Type.BOLD
|
||||
),
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(
|
||||
start = 5,
|
||||
endInclusive = 9
|
||||
),
|
||||
type = Block.Content.Text.Mark.Type.ITALIC
|
||||
),
|
||||
Block.Content.Text.Mark(
|
||||
range = IntRange(
|
||||
start = 14,
|
||||
endInclusive = 18
|
||||
),
|
||||
type = Block.Content.Text.Mark.Type.STRIKETHROUGH
|
||||
)
|
||||
),
|
||||
style = Block.Content.Text.Style.P
|
||||
)
|
||||
)
|
||||
|
||||
val page = Block(
|
||||
id = root,
|
||||
fields = Block.Fields(emptyMap()),
|
||||
content = Block.Content.Smart(
|
||||
type = Block.Content.Smart.Type.PAGE
|
||||
),
|
||||
children = listOf(a.id)
|
||||
)
|
||||
|
||||
val document = listOf(page, a)
|
||||
|
||||
stubOpenDocument(document)
|
||||
stubObserveEvents()
|
||||
|
||||
val vm = buildViewModel()
|
||||
|
||||
vm.open(root)
|
||||
|
||||
vm.apply {
|
||||
onBlockFocusChanged(
|
||||
id = a.id,
|
||||
hasFocus = true
|
||||
)
|
||||
onSelectionChanged(
|
||||
id = a.id,
|
||||
selection = IntRange(12, 12)
|
||||
)
|
||||
onMentionEvent(
|
||||
MentionEvent.MentionSuggestStart(
|
||||
cursorCoordinate = 500,
|
||||
mentionStart = from
|
||||
)
|
||||
)
|
||||
onMentionSuggestClick(
|
||||
mention = Mention(
|
||||
id = mentionHash,
|
||||
emoji = null,
|
||||
image = null,
|
||||
title = mentionText
|
||||
),
|
||||
mentionTrigger = mentionTrigger
|
||||
)
|
||||
}
|
||||
|
||||
vm.state.test().apply {
|
||||
assertValue(
|
||||
ViewState.Success(
|
||||
blocks = listOf(
|
||||
BlockView.Title(
|
||||
id = root,
|
||||
isFocused = false,
|
||||
text = null,
|
||||
mode = BlockView.Mode.EDIT
|
||||
),
|
||||
BlockView.Paragraph(
|
||||
id = a.id,
|
||||
cursor = 28,
|
||||
isSelected = false,
|
||||
isFocused = true,
|
||||
marks = listOf(
|
||||
Markup.Mark(
|
||||
from = 0,
|
||||
to = 3,
|
||||
type = Markup.Type.BOLD
|
||||
),
|
||||
Markup.Mark(
|
||||
from = 5,
|
||||
to = 9,
|
||||
type = Markup.Type.ITALIC
|
||||
),
|
||||
Markup.Mark(
|
||||
from = 29,
|
||||
to = 33,
|
||||
type = Markup.Type.STRIKETHROUGH
|
||||
),
|
||||
Markup.Mark(
|
||||
from = from,
|
||||
to = from + mentionText.length,
|
||||
type = Markup.Type.MENTION,
|
||||
param = mentionHash
|
||||
)
|
||||
),
|
||||
backgroundColor = null,
|
||||
color = null,
|
||||
indent = 0,
|
||||
text = "page about Avant-Garde Jazz music",
|
||||
mode = BlockView.Mode.EDIT
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
clearPendingCoroutines()
|
||||
}
|
||||
|
||||
private fun clearPendingCoroutines() {
|
||||
coroutineTestRule.advanceTime(PageViewModel.TEXT_CHANGES_DEBOUNCE_DURATION)
|
||||
}
|
||||
}
|
|
@ -109,6 +109,12 @@ class EditorMultiSelectModeTest : EditorPresentationTestSetup() {
|
|||
isVisible = true,
|
||||
isScrollAndMoveEnabled = false,
|
||||
count = 0
|
||||
),
|
||||
mentionToolbar = ControlPanelState.Toolbar.MentionToolbar(
|
||||
isVisible = false,
|
||||
cursorCoordinate = null,
|
||||
mentionFilter = null,
|
||||
mentionFrom = null
|
||||
)
|
||||
)
|
||||
)
|
||||
|
@ -205,6 +211,12 @@ class EditorMultiSelectModeTest : EditorPresentationTestSetup() {
|
|||
isVisible = true,
|
||||
isScrollAndMoveEnabled = false,
|
||||
count = 0
|
||||
),
|
||||
mentionToolbar = ControlPanelState.Toolbar.MentionToolbar(
|
||||
isVisible = false,
|
||||
cursorCoordinate = null,
|
||||
mentionFilter = null,
|
||||
mentionFrom = null
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
|
@ -17,6 +17,7 @@ import com.agileburo.anytype.domain.event.model.Payload
|
|||
import com.agileburo.anytype.domain.misc.UrlBuilder
|
||||
import com.agileburo.anytype.domain.page.*
|
||||
import com.agileburo.anytype.domain.page.bookmark.SetupBookmark
|
||||
import com.agileburo.anytype.domain.page.navigation.GetListPages
|
||||
import com.agileburo.anytype.presentation.page.DocumentExternalEventReducer
|
||||
import com.agileburo.anytype.presentation.page.Editor
|
||||
import com.agileburo.anytype.presentation.page.PageViewModel
|
||||
|
@ -49,6 +50,9 @@ open class EditorPresentationTestSetup {
|
|||
@Mock
|
||||
lateinit var updateText: UpdateText
|
||||
|
||||
@Mock
|
||||
lateinit var getListPages: GetListPages
|
||||
|
||||
@Mock
|
||||
lateinit var updateCheckbox: UpdateCheckbox
|
||||
|
||||
|
@ -138,6 +142,7 @@ open class EditorPresentationTestSetup {
|
|||
)
|
||||
|
||||
return PageViewModel(
|
||||
getListPages = getListPages,
|
||||
openPage = openPage,
|
||||
closePage = closePage,
|
||||
createPage = createPage,
|
||||
|
|
|
@ -114,6 +114,12 @@ class EditorScrollAndMoveTest : EditorPresentationTestSetup() {
|
|||
isVisible = true,
|
||||
isScrollAndMoveEnabled = true,
|
||||
count = 1
|
||||
),
|
||||
mentionToolbar = ControlPanelState.Toolbar.MentionToolbar(
|
||||
isVisible = false,
|
||||
cursorCoordinate = null,
|
||||
mentionFilter = null,
|
||||
mentionFrom = null
|
||||
)
|
||||
)
|
||||
)
|
||||
|
@ -206,6 +212,12 @@ class EditorScrollAndMoveTest : EditorPresentationTestSetup() {
|
|||
isVisible = true,
|
||||
isScrollAndMoveEnabled = false,
|
||||
count = 1
|
||||
),
|
||||
mentionToolbar = ControlPanelState.Toolbar.MentionToolbar(
|
||||
isVisible = false,
|
||||
cursorCoordinate = null,
|
||||
mentionFilter = null,
|
||||
mentionFrom = null
|
||||
)
|
||||
)
|
||||
)
|
||||
|
@ -302,6 +314,12 @@ class EditorScrollAndMoveTest : EditorPresentationTestSetup() {
|
|||
isVisible = true,
|
||||
isScrollAndMoveEnabled = false,
|
||||
count = 0
|
||||
),
|
||||
mentionToolbar = ControlPanelState.Toolbar.MentionToolbar(
|
||||
isVisible = false,
|
||||
cursorCoordinate = null,
|
||||
mentionFilter = null,
|
||||
mentionFrom = null
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
|
@ -37,7 +37,8 @@ class SpanActivity : AppCompatActivity() {
|
|||
R.drawable.ic_baseline_4k_24,
|
||||
resource,
|
||||
16.px,
|
||||
0
|
||||
0,
|
||||
""
|
||||
),
|
||||
8,
|
||||
9,
|
||||
|
@ -65,7 +66,8 @@ class SpanActivity : AppCompatActivity() {
|
|||
context = this,
|
||||
mResourceId = R.drawable.ic_mention_deafult,
|
||||
imageSize = 16.px,
|
||||
imagePadding = 0
|
||||
imagePadding = 0,
|
||||
param = ""
|
||||
),
|
||||
0,
|
||||
6,
|
||||
|
@ -77,7 +79,7 @@ class SpanActivity : AppCompatActivity() {
|
|||
|
||||
val spannableString2 = SpannableString("Строка с маркапом и болдом")
|
||||
spannableString2.setSpan(
|
||||
MentionSpan(this, R.drawable.ic_baseline_4k_24, null, 16.px, 0),
|
||||
MentionSpan(this, R.drawable.ic_baseline_4k_24, null, 16.px, 0, ""),
|
||||
9,
|
||||
10,
|
||||
0
|
||||
|
@ -86,7 +88,7 @@ class SpanActivity : AppCompatActivity() {
|
|||
|
||||
val spannableString3 = SpannableString("Строка с маркапом и болдом")
|
||||
spannableString3.setSpan(
|
||||
MentionSpan(this, R.drawable.ic_baseline_4k_24, null, 16.px, 0),
|
||||
MentionSpan(this, R.drawable.ic_baseline_4k_24, null, 16.px, 0, ""),
|
||||
9,
|
||||
17,
|
||||
0
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue