use recyclerview for markwon

This commit is contained in:
k0shk0sh 2019-07-24 19:00:08 +02:00
parent efe7a89714
commit aae3d0fd09
19 changed files with 151 additions and 242 deletions

View File

@ -13,21 +13,25 @@ import com.fastaccess.github.ui.modules.issue.fragment.IssueFragment
import com.fastaccess.github.usecase.search.FilterSearchUsersUseCase
import com.fastaccess.github.utils.extensions.theme
import com.fastaccess.markdown.GrammarLocatorDef
import com.fastaccess.markdown.extension.markwon.emoji.EmojiPlugin
import dagger.Module
import dagger.Provides
import io.noties.markwon.Markwon
import io.noties.markwon.ext.latex.JLatexMathPlugin
import io.noties.markwon.ext.strikethrough.StrikethroughPlugin
import io.noties.markwon.ext.tables.TablePlugin
import io.noties.markwon.ext.tasklist.TaskListPlugin
import io.noties.markwon.html.HtmlPlugin
import io.noties.markwon.image.glide.GlideImagesPlugin
import io.noties.markwon.linkify.LinkifyPlugin
import io.noties.markwon.recycler.MarkwonAdapter
import io.noties.markwon.recycler.SimpleEntry
import io.noties.markwon.recycler.table.TableEntry
import io.noties.markwon.recycler.table.TableEntryPlugin
import io.noties.markwon.syntax.Prism4jThemeDarkula
import io.noties.markwon.syntax.Prism4jThemeDefault
import io.noties.markwon.syntax.SyntaxHighlightPlugin
import io.noties.prism4j.Prism4j
import org.commonmark.ext.gfm.tables.TableBlock
import org.commonmark.node.FencedCodeBlock
/**
* Created by Kosh on 02.02.19.
@ -46,7 +50,7 @@ class FragmentModule {
.usePlugin(TaskListPlugin.create(context))
.usePlugin(HtmlPlugin.create())
.usePlugin(GlideImagesPlugin.create(context))
.usePlugin(TablePlugin.create(context))
.usePlugin(TableEntryPlugin.create(context))
.usePlugin(StrikethroughPlugin.create())
.usePlugin(LinkifyPlugin.create(Linkify.EMAIL_ADDRESSES or Linkify.WEB_URLS))
.usePlugin(
@ -58,9 +62,16 @@ class FragmentModule {
}
)
)
.usePlugin(EmojiPlugin.create())
.build()
@PerFragment @Provides fun provideMarkwonAdapterBuilder(): MarkwonAdapter.Builder =
MarkwonAdapter.builder(R.layout.markdown_textview_row_item, R.id.text)
.include(FencedCodeBlock::class.java, SimpleEntry.create(R.layout.markwon_fenced_cod_block_row_item, R.id.text))
.include(TableBlock::class.java, TableEntry.create {
it.tableLayout(R.layout.markwon_table_row_item, R.id.table_layout)
.textLayoutIsRoot(R.layout.markwon_table_entry_cell)
})
@PerFragment @Provides fun provideMentionsPresenter(
context: Context,
searchUsersUseCase: FilterSearchUsersUseCase

View File

@ -11,13 +11,15 @@ import com.fastaccess.github.ui.adapter.viewholder.CommentViewHolder
import com.fastaccess.github.ui.adapter.viewholder.IssueContentViewHolder
import com.fastaccess.github.ui.adapter.viewholder.LoadingViewHolder
import io.noties.markwon.Markwon
import io.noties.markwon.recycler.MarkwonAdapter
/**
* Created by Kosh on 20.01.19.
*/
class IssueTimelineAdapter(
private val markwon: Markwon,
private val theme: Int
private val theme: Int,
private val markwonAdapterBuilder: MarkwonAdapter.Builder
) : ListAdapter<TimelineModel, RecyclerView.ViewHolder>(DIFF_CALLBACK) {
private val notifyCallback by lazy {
@ -36,15 +38,21 @@ class IssueTimelineAdapter(
} ?: super.getItemViewType(position)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int
): RecyclerView.ViewHolder {
return when (viewType) {
COMMENT -> CommentViewHolder(parent, markwon, theme, notifyCallback)
COMMENT -> CommentViewHolder(parent, markwon, theme, notifyCallback, markwonAdapterBuilder)
CONTENT -> IssueContentViewHolder(parent)
else -> LoadingViewHolder<Any>(parent).apply { itemView.isVisible = false }
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
override fun onBindViewHolder(
holder: RecyclerView.ViewHolder,
position: Int
) {
when (holder) {
is CommentViewHolder -> holder.bind(getItem(position).comment)
is IssueContentViewHolder -> holder.bind(getItem(position))
@ -68,8 +76,15 @@ class IssueTimelineAdapter(
private const val CONTENT = 3
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<TimelineModel?>() {
override fun areItemsTheSame(oldItem: TimelineModel, newItem: TimelineModel): Boolean = oldItem.hashCode() == newItem.hashCode()
override fun areContentsTheSame(oldItem: TimelineModel, newItem: TimelineModel): Boolean = oldItem == newItem
override fun areItemsTheSame(
oldItem: TimelineModel,
newItem: TimelineModel
): Boolean = oldItem.hashCode() == newItem.hashCode()
override fun areContentsTheSame(
oldItem: TimelineModel,
newItem: TimelineModel
): Boolean = oldItem == newItem
}
}
}

View File

@ -1,22 +1,19 @@
package com.fastaccess.github.ui.adapter.viewholder
import android.annotation.SuppressLint
import android.text.util.Linkify
import android.util.Patterns
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.view.isVisible
import com.fastaccess.data.model.CommentAuthorAssociation
import com.fastaccess.data.model.CommentModel
import com.fastaccess.data.model.getEmoji
import com.fastaccess.github.base.engine.ThemeEngine
import com.fastaccess.github.R
import com.fastaccess.github.extensions.timeAgo
import com.fastaccess.github.ui.adapter.base.BaseViewHolder
import com.fastaccess.github.utils.extensions.popupEmoji
import com.fastaccess.markdown.MarkdownProvider
import io.noties.markwon.Markwon
import io.noties.markwon.recycler.MarkwonAdapter
import kotlinx.android.synthetic.main.comment_row_item.view.*
import java.util.regex.Pattern
/**
@ -27,10 +24,11 @@ class CommentViewHolder(
parent: ViewGroup,
private val markwon: Markwon,
private val theme: Int,
private val callback: (position: Int) -> Unit
private val callback: (position: Int) -> Unit,
private val markwonAdapterBuilder: MarkwonAdapter.Builder
) : BaseViewHolder<CommentModel?>(
LayoutInflater.from(parent.context)
.inflate(com.fastaccess.github.R.layout.comment_row_item, parent, false)
.inflate(R.layout.comment_row_item, parent, false)
) {
@SuppressLint("SetTextI18n")
@ -47,24 +45,23 @@ class CommentViewHolder(
} else {
"${model.authorAssociation?.value?.toLowerCase()?.replace("_", "")} ${model.updatedAt?.timeAgo()}"
}
val adapter = markwonAdapterBuilder.build()
descriptionRecyclerView.adapter = adapter
adapter.setMarkdown(markwon, model.body ?: resources.getString(R.string.no_description_provided))
adapter.notifyDataSetChanged()
MarkdownProvider.loadIntoTextView(
markwon, description, model.body ?: "", ThemeEngine.getCodeBackground(theme),
ThemeEngine.isLightTheme(theme)
)
val filter = Linkify.TransformFilter { match, _ -> match.group() }
val mentionPattern = Pattern.compile("@([A-Za-z0-9_-]+)")
val mentionScheme = "https://www.github.com/"
Linkify.addLinks(description, mentionPattern, mentionScheme, null, filter)
val hashtagPattern = Pattern.compile("#([A-Za-z0-9_-]+)")
val hashtagScheme = "https://www.github.com/"
Linkify.addLinks(description, hashtagPattern, hashtagScheme, null, filter)
val urlPattern = Patterns.WEB_URL
Linkify.addLinks(description, urlPattern, null, null, filter)
// val filter = Linkify.TransformFilter { match, _ -> match.group() }
// val mentionPattern = Pattern.compile("@([A-Za-z0-9_-]+)")
// val mentionScheme = "https://www.github.com/"
// Linkify.addLinks(description, mentionPattern, mentionScheme, null, filter)
//
// val hashtagPattern = Pattern.compile("#([A-Za-z0-9_-]+)")
// val hashtagScheme = "https://www.github.com/"
// Linkify.addLinks(description, hashtagPattern, hashtagScheme, null, filter)
//
// val urlPattern = Patterns.WEB_URL
// Linkify.addLinks(description, urlPattern, null, null, filter)
addEmoji.setOnClickListener {
it.popupEmoji(requireNotNull(model.id), model.reactionGroups) {

View File

@ -21,7 +21,6 @@ import com.fastaccess.data.storage.FastHubSharedPreference
import com.fastaccess.github.R
import com.fastaccess.github.base.BaseFragment
import com.fastaccess.github.base.BaseViewModel
import com.fastaccess.github.base.engine.ThemeEngine
import com.fastaccess.github.extensions.*
import com.fastaccess.github.platform.mentions.MentionsPresenter
import com.fastaccess.github.ui.adapter.IssueTimelineAdapter
@ -38,7 +37,6 @@ import com.fastaccess.github.utils.WEB_EDITOR_DEEPLINK
import com.fastaccess.github.utils.extensions.isConnected
import com.fastaccess.github.utils.extensions.popupEmoji
import com.fastaccess.github.utils.extensions.theme
import com.fastaccess.markdown.MarkdownProvider
import com.fastaccess.markdown.spans.LabelSpan
import com.fastaccess.markdown.widget.SpannableBuilder
import com.google.android.material.appbar.AppBarLayout
@ -49,6 +47,7 @@ import github.type.CommentAuthorAssociation
import github.type.IssueState
import github.type.LockReason
import io.noties.markwon.Markwon
import io.noties.markwon.recycler.MarkwonAdapter
import kotlinx.android.synthetic.main.empty_state_layout.*
import kotlinx.android.synthetic.main.issue_header_row_item.*
import kotlinx.android.synthetic.main.issue_pr_fragment_layout.*
@ -66,12 +65,14 @@ class IssueFragment : BaseFragment(), LockUnlockFragment.OnLockReasonSelected,
@Inject lateinit var markwon: Markwon
@Inject lateinit var preference: FastHubSharedPreference
@Inject lateinit var mentionsPresenter: MentionsPresenter
@Inject lateinit var markwonAdapterBuilder: MarkwonAdapter.Builder
private val viewModel by lazy { ViewModelProviders.of(this, viewModelFactory).get(IssueTimelineViewModel::class.java) }
private val login by lazy { arguments?.getString(EXTRA) ?: "" }
private val repo by lazy { arguments?.getString(EXTRA_TWO) ?: "" }
private val number by lazy { arguments?.getInt(EXTRA_THREE) ?: 0 }
private val adapter by lazy { IssueTimelineAdapter(markwon, preference.theme) }
private val markwonAdapter by lazy { markwonAdapterBuilder.build() }
private val adapter by lazy { IssueTimelineAdapter(markwon, preference.theme, markwonAdapterBuilder) }
override fun layoutRes(): Int = R.layout.issue_pr_fragment_layout
override fun viewModel(): BaseViewModel? = viewModel
@ -239,10 +240,11 @@ class IssueFragment : BaseFragment(), LockUnlockFragment.OnLockReasonSelected,
} else {
"${model.authorAssociation?.toLowerCase()?.replace("_", "")} ${model.updatedAt?.timeAgo()}"
}
MarkdownProvider.loadIntoTextView(
markwon, description, model.body ?: "", ThemeEngine.getCodeBackground(theme),
ThemeEngine.isLightTheme(theme)
)
descriptionRecyclerView.adapter = markwonAdapter
markwonAdapter.setMarkdown(markwon, model.body ?: "**${getString(R.string.no_description_provided)}**")
markwonAdapter.notifyDataSetChanged()
state.text = model.state?.toLowerCase()
state.setChipBackgroundColorResource(
if (IssueState.OPEN.rawValue().equals(model.state, true)) {

View File

@ -7,19 +7,28 @@ import com.fastaccess.github.R
import com.fastaccess.github.extensions.getDrawableCompat
import com.fastaccess.github.extensions.route
import com.fastaccess.github.platform.glide.GlideApp
import timber.log.Timber
/**
* Created by Kosh on 27.12.18.
*/
class AvatarImageView constructor(context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0) : AppCompatImageView(context, attrs, defStyle) {
class AvatarImageView constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0
) : AppCompatImageView(context, attrs, defStyle) {
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(
context: Context,
attrs: AttributeSet?
) : this(context, attrs, 0)
fun loadAvatar(url: String? = null, userUrl: String? = null) {
fun loadAvatar(
url: String? = null,
userUrl: String? = null
) {
setBackgroundResource(R.drawable.circle_shape)
if (url.isNullOrEmpty()) {
setImageResource(R.drawable.ic_profile)
@ -34,6 +43,9 @@ class AvatarImageView constructor(context: Context,
.dontAnimate()
.into(this)
}
userUrl?.let { setOnClickListener { it.context.route(userUrl) } }
userUrl?.let { userUrl ->
Timber.e(userUrl)
setOnClickListener { it.context.route(userUrl) }
}
}
}

View File

@ -38,7 +38,7 @@ class BaseRecyclerView constructor(context: Context,
}
}
override fun setAdapter(adapter: RecyclerView.Adapter<*>?) {
override fun setAdapter(adapter: Adapter<*>?) {
super.setAdapter(adapter)
if (isInEditMode) return
if (adapter != null) {

View File

@ -95,14 +95,14 @@
android:src="@drawable/ic_overflow" />
</LinearLayout>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/description"
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/descriptionRecyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
android:textIsSelectable="true"
tools:text="Place the truffels in an ice blender, and toss immediately with tangy lime.immediately with tangy lime" />
android:layout_marginTop="@dimen/spacing_micro"
android:nestedScrollingEnabled="false"
android:orientation="vertical"
app:layoutManager="@string/llm" />
</LinearLayout>
</FrameLayout>

View File

@ -151,15 +151,14 @@
android:src="@drawable/ic_overflow" />
</LinearLayout>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/description"
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/descriptionRecyclerView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="@dimen/spacing_micro"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
android:textIsSelectable="true"
tools:text="Place the truffels in an ice blender, and toss immediately with tangy lime.immediately with tangy lime" />
android:nestedScrollingEnabled="false"
android:orientation="vertical"
app:layoutManager="@string/llm" />
</LinearLayout>
<LinearLayout

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.AppCompatTextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Subhead"
android:textIsSelectable="true" />

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<HorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false"
android:fillViewport="true"
android:nestedScrollingEnabled="false"
android:scrollbarStyle="outsideInset">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/text"
style="@style/LayoutPadding"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fontFamily="monospace"
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
</HorizontalScrollView>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
tools:text="Table content" />

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal" />

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<HorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false"
android:nestedScrollingEnabled="false"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:scrollbarStyle="outsideInset">
<TableLayout
android:id="@+id/table_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:stretchColumns="*" />
</HorizontalScrollView>

View File

@ -1,22 +0,0 @@
package com.fastaccess.markdown.extension.markwon.emoji
import org.commonmark.node.CustomNode
import org.commonmark.node.Delimited
/**
* Created by kosh on 20/08/2017.
*/
class Emoji : CustomNode(), Delimited {
var emoji: String? = null
override fun getOpeningDelimiter(): String = DELIMITER
override fun getClosingDelimiter(): String = DELIMITER
override fun toString(): String = emoji ?: "no emoji"
companion object {
private const val DELIMITER = ":"
}
}

View File

@ -1,29 +0,0 @@
package com.fastaccess.markdown.extension.markwon.emoji
import com.fastaccess.markdown.extension.markwon.emoji.internal.EmojiDelimiterProcessor
import com.fastaccess.markdown.extension.markwon.emoji.internal.EmojiNodeRenderer
import org.commonmark.Extension
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer
/**
* Created by kosh on 20/08/2017.
*/
class EmojiExtension private constructor() : Parser.ParserExtension, HtmlRenderer.HtmlRendererExtension {
override fun extend(parserBuilder: Parser.Builder) {
parserBuilder.customDelimiterProcessor(EmojiDelimiterProcessor())
}
override fun extend(rendererBuilder: HtmlRenderer.Builder) {
rendererBuilder.nodeRendererFactory { EmojiNodeRenderer(it) }
}
companion object {
fun create(): Extension {
return EmojiExtension()
}
}
}

View File

@ -1,33 +0,0 @@
package com.fastaccess.markdown.extension.markwon.emoji
import android.text.SpannedString
import com.fastaccess.markdown.emoji.EmojiManager
import io.noties.markwon.AbstractMarkwonPlugin
import io.noties.markwon.MarkwonVisitor
import org.commonmark.parser.Parser
import timber.log.Timber
class EmojiPlugin : AbstractMarkwonPlugin() {
override fun configureParser(builder: Parser.Builder) {
builder.extensions(setOf(EmojiExtension.create()))
}
override fun configureVisitor(builder: MarkwonVisitor.Builder) {
builder.on(Emoji::class.java) { visitor, emoji ->
val length = visitor.length()
val emojiUnicode = emoji.emoji
val unicode = EmojiManager.getForAlias(emoji.emoji)?.unicode
if (!unicode.isNullOrEmpty()) {
visitor.setSpans(length, SpannedString(unicode))
} else {
Timber.e(emojiUnicode)
}
}
}
companion object {
fun create() = EmojiPlugin()
}
}

View File

@ -1,30 +0,0 @@
package com.fastaccess.markdown.extension.markwon.emoji
import android.text.SpannedString
import com.fastaccess.markdown.emoji.EmojiManager
import io.noties.markwon.MarkwonConfiguration
import io.noties.markwon.Prop
import io.noties.markwon.RenderProps
import io.noties.markwon.SpanFactory
import timber.log.Timber
/**
* Created by Kosh on 2019-07-20.
*/
class EmojiSpanFactory : SpanFactory {
override fun getSpans(
configuration: MarkwonConfiguration,
props: RenderProps
): Any? {
val emoji = props.get<Emoji>(Prop.of(":"))
Timber.e("$props $emoji")
if (emoji != null) {
val unicode = EmojiManager.getForAlias(emoji.emoji)
if (unicode?.unicode != null) {
return SpannedString(unicode.unicode)
}
}
return null
}
}

View File

@ -1,40 +0,0 @@
package com.fastaccess.markdown.extension.markwon.emoji.internal
import com.fastaccess.markdown.extension.markwon.emoji.Emoji
import org.commonmark.node.Text
import org.commonmark.parser.delimiter.DelimiterProcessor
import org.commonmark.parser.delimiter.DelimiterRun
class EmojiDelimiterProcessor : DelimiterProcessor {
override fun getOpeningCharacter(): Char = ':'
override fun getClosingCharacter(): Char = ':'
override fun getMinLength(): Int = 1
override fun getDelimiterUse(
opener: DelimiterRun,
closer: DelimiterRun
): Int = if (opener.length() >= 1 && closer.length() >= 1) {
1
} else {
0
}
override fun process(
opener: Text,
closer: Text,
delimiterCount: Int
) {
var emoji: Emoji? = null
val text = opener.next
if (text is Text) {
emoji = Emoji()
emoji.emoji = text.literal
text.unlink()
}
if (emoji != null) opener.insertAfter(emoji)
}
}

View File

@ -1,29 +0,0 @@
package com.fastaccess.markdown.extension.markwon.emoji.internal
import com.fastaccess.markdown.extension.markwon.emoji.Emoji
import org.commonmark.node.Node
import org.commonmark.renderer.NodeRenderer
import org.commonmark.renderer.html.HtmlNodeRendererContext
import org.commonmark.renderer.html.HtmlWriter
class EmojiNodeRenderer(private val context: HtmlNodeRendererContext) : NodeRenderer {
private val html: HtmlWriter = context.writer
override fun getNodeTypes(): Set<Class<out Node>> = setOf<Class<out Node>>(Emoji::class.java)
override fun render(node: Node) {
val attributes = context.extendAttributes(node, "emoji", emptyMap())
html.tag("emoji", attributes)
renderChildren(node)
html.tag("/emoji")
}
private fun renderChildren(parent: Node) {
var node: Node? = parent.firstChild
while (node != null) {
val next = node.next
context.render(node)
node = next
}
}
}