Merge branch 'feature/html_rendering' into develop

This commit is contained in:
ganfra 2019-02-27 17:51:06 +01:00
commit 753e70775a
38 changed files with 801 additions and 77 deletions

View File

@ -60,12 +60,16 @@ dependencies {
def epoxy_version = "3.0.0" def epoxy_version = "3.0.0"
def arrow_version = "0.8.2" def arrow_version = "0.8.2"
def coroutines_version = "1.0.1"
def markwon_version = '3.0.0-SNAPSHOT'
implementation project(":matrix-sdk-android") implementation project(":matrix-sdk-android")
implementation project(":matrix-sdk-android-rx") implementation project(":matrix-sdk-android-rx")
implementation 'com.android.support:multidex:1.0.3' implementation 'com.android.support:multidex:1.0.3'
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
implementation 'androidx.appcompat:appcompat:1.1.0-alpha01' implementation 'androidx.appcompat:appcompat:1.1.0-alpha01'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
@ -97,6 +101,9 @@ dependencies {
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
implementation 'com.google.android.material:material:1.1.0-alpha02' implementation 'com.google.android.material:material:1.1.0-alpha02'
implementation 'me.gujun.android:span:1.7' implementation 'me.gujun.android:span:1.7'
implementation "ru.noties.markwon:core:$markwon_version"
implementation "ru.noties.markwon:html:$markwon_version"
// DI // DI
implementation "org.koin:koin-android:$koin_version" implementation "org.koin:koin-android:$koin_version"

View File

@ -29,4 +29,8 @@ fun AppCompatActivity.replaceFragment(fragment: Fragment, frameId: Int) {
fun AppCompatActivity.addFragmentToBackstack(fragment: Fragment, frameId: Int, tag: String? = null) { fun AppCompatActivity.addFragmentToBackstack(fragment: Fragment, frameId: Int, tag: String? = null) {
supportFragmentManager.inTransaction { replace(frameId, fragment).addToBackStack(tag) } supportFragmentManager.inTransaction { replace(frameId, fragment).addToBackStack(tag) }
}
fun AppCompatActivity.hideKeyboard() {
currentFocus?.hideKeyboard()
} }

View File

@ -17,12 +17,9 @@
package im.vector.riotredesign.core.extensions package im.vector.riotredesign.core.extensions
import im.vector.matrix.android.api.session.events.model.Event import im.vector.matrix.android.api.session.events.model.Event
import org.threeten.bp.Instant import im.vector.riotredesign.core.resources.DateProvider
import org.threeten.bp.LocalDateTime import org.threeten.bp.LocalDateTime
import org.threeten.bp.ZoneId
fun Event.localDateTime(): LocalDateTime { fun Event.localDateTime(): LocalDateTime {
val instant = Instant.ofEpochMilli(originServerTs ?: 0) return DateProvider.toLocalDateTime(originServerTs)
return LocalDateTime.ofInstant(instant, ZoneId.systemDefault())
} }

View File

@ -0,0 +1,32 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.core.resources
import org.threeten.bp.Instant
import org.threeten.bp.LocalDateTime
import org.threeten.bp.ZoneId
object DateProvider {
private val zoneId = ZoneId.systemDefault()
fun toLocalDateTime(timestamp: Long?): LocalDateTime {
val instant = Instant.ofEpochMilli(timestamp ?: 0)
return LocalDateTime.ofInstant(instant, zoneId)
}
}

View File

@ -16,16 +16,21 @@
package im.vector.riotredesign.features.home package im.vector.riotredesign.features.home
import android.content.Context
import android.graphics.drawable.Drawable
import android.widget.ImageView import android.widget.ImageView
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import com.amulyakhare.textdrawable.TextDrawable import com.amulyakhare.textdrawable.TextDrawable
import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.RequestOptions
import im.vector.matrix.android.api.Matrix import im.vector.matrix.android.api.Matrix
import im.vector.matrix.android.api.MatrixPatterns
import im.vector.matrix.android.api.session.room.model.RoomMember import im.vector.matrix.android.api.session.room.model.RoomMember
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.extensions.firstCharAsString
import im.vector.riotredesign.core.glide.GlideApp import im.vector.riotredesign.core.glide.GlideApp
import im.vector.riotredesign.core.glide.GlideRequest
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
object AvatarRenderer { object AvatarRenderer {
@ -41,17 +46,47 @@ object AvatarRenderer {
if (name.isNullOrEmpty()) { if (name.isNullOrEmpty()) {
return return
} }
val resolvedUrl = Matrix.getInstance().currentSession.contentUrlResolver().resolveFullSize(avatarUrl) val placeholder = buildPlaceholderDrawable(imageView.context, name)
val avatarColor = ContextCompat.getColor(imageView.context, R.color.pale_teal) buildGlideRequest(imageView.context, avatarUrl)
val fallbackDrawable = TextDrawable.builder().buildRound(name.firstCharAsString().toUpperCase(), avatarColor) .placeholder(placeholder)
GlideApp
.with(imageView)
.load(resolvedUrl)
.placeholder(fallbackDrawable)
.apply(RequestOptions.circleCropTransform())
.into(imageView) .into(imageView)
} }
fun load(context: Context, avatarUrl: String?, name: String?, size: Int, callback: Callback) {
if (name.isNullOrEmpty()) {
return
}
val request = buildGlideRequest(context, avatarUrl)
GlobalScope.launch {
val placeholder = buildPlaceholderDrawable(context, name)
callback.onDrawableUpdated(placeholder)
try {
val drawable = request.submit(size, size).get()
callback.onDrawableUpdated(drawable)
} catch (exception: Exception) {
callback.onDrawableUpdated(placeholder)
}
}
}
private fun buildGlideRequest(context: Context, avatarUrl: String?): GlideRequest<Drawable> {
val resolvedUrl = Matrix.getInstance().currentSession.contentUrlResolver().resolveFullSize(avatarUrl)
return GlideApp
.with(context)
.load(resolvedUrl)
.apply(RequestOptions.circleCropTransform())
}
private fun buildPlaceholderDrawable(context: Context, name: String): Drawable {
val avatarColor = ContextCompat.getColor(context, R.color.pale_teal)
val isNameUserId = MatrixPatterns.isUserId(name)
val firstLetterIndex = if (isNameUserId) 1 else 0
val firstLetter = name[firstLetterIndex].toString().toUpperCase()
return TextDrawable.builder().buildRound(firstLetter, avatarColor)
}
interface Callback {
fun onDrawableUpdated(drawable: Drawable?)
}
} }

View File

@ -24,9 +24,11 @@ import android.view.MenuItem
import androidx.appcompat.app.ActionBarDrawerToggle import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.core.view.GravityCompat import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import com.airbnb.mvrx.viewModel import com.airbnb.mvrx.viewModel
import im.vector.riotredesign.R import im.vector.riotredesign.R
import im.vector.riotredesign.core.extensions.hideKeyboard
import im.vector.riotredesign.core.extensions.observeEvent import im.vector.riotredesign.core.extensions.observeEvent
import im.vector.riotredesign.core.extensions.replaceFragment import im.vector.riotredesign.core.extensions.replaceFragment
import im.vector.riotredesign.core.platform.OnBackPressed import im.vector.riotredesign.core.platform.OnBackPressed
@ -44,11 +46,18 @@ class HomeActivity : RiotActivity(), ToolbarConfigurable {
private val homeActivityViewModel: HomeActivityViewModel by viewModel() private val homeActivityViewModel: HomeActivityViewModel by viewModel()
private val homeNavigator by inject<HomeNavigator>() private val homeNavigator by inject<HomeNavigator>()
private val drawerListener = object : DrawerLayout.SimpleDrawerListener() {
override fun onDrawerStateChanged(newState: Int) {
hideKeyboard()
}
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
loadKoinModules(listOf(HomeModule().definition)) loadKoinModules(listOf(HomeModule(this).definition))
homeNavigator.activity = this homeNavigator.activity = this
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home) setContentView(R.layout.activity_home)
drawerLayout.addDrawerListener(drawerListener)
if (savedInstanceState == null) { if (savedInstanceState == null) {
val homeDrawerFragment = HomeDrawerFragment.newInstance() val homeDrawerFragment = HomeDrawerFragment.newInstance()
val loadingDetail = LoadingRoomDetailFragment.newInstance() val loadingDetail = LoadingRoomDetailFragment.newInstance()
@ -61,6 +70,7 @@ class HomeActivity : RiotActivity(), ToolbarConfigurable {
} }
override fun onDestroy() { override fun onDestroy() {
drawerLayout.removeDrawerListener(drawerListener)
homeNavigator.activity = null homeNavigator.activity = null
super.onDestroy() super.onDestroy()
} }

View File

@ -16,24 +16,43 @@
package im.vector.riotredesign.features.home package im.vector.riotredesign.features.home
import im.vector.matrix.android.api.Matrix
import im.vector.riotredesign.features.home.group.SelectedGroupStore import im.vector.riotredesign.features.home.group.SelectedGroupStore
import im.vector.riotredesign.features.home.room.VisibleRoomStore import im.vector.riotredesign.features.home.room.VisibleRoomStore
import im.vector.riotredesign.features.home.room.detail.timeline.* import im.vector.riotredesign.features.home.room.detail.timeline.CallItemFactory
import im.vector.riotredesign.features.home.room.detail.timeline.DefaultItemFactory
import im.vector.riotredesign.features.home.room.detail.timeline.MessageItemFactory
import im.vector.riotredesign.features.home.room.detail.timeline.RoomHistoryVisibilityItemFactory
import im.vector.riotredesign.features.home.room.detail.timeline.RoomMemberItemFactory
import im.vector.riotredesign.features.home.room.detail.timeline.RoomNameItemFactory
import im.vector.riotredesign.features.home.room.detail.timeline.RoomTopicItemFactory
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineDateFormatter
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineEventController
import im.vector.riotredesign.features.home.room.detail.timeline.TimelineItemFactory
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
import im.vector.riotredesign.features.home.room.list.RoomSummaryComparator import im.vector.riotredesign.features.home.room.list.RoomSummaryComparator
import im.vector.riotredesign.features.home.room.list.RoomSummaryController import im.vector.riotredesign.features.home.room.list.RoomSummaryController
import im.vector.riotredesign.features.html.EventHtmlRenderer
import org.koin.dsl.module.module import org.koin.dsl.module.module
class HomeModule { class HomeModule(homeActivity: HomeActivity) {
val definition = module(override = true) { val definition = module(override = true) {
single {
Matrix.getInstance().currentSession
}
single { single {
TimelineDateFormatter(get()) TimelineDateFormatter(get())
} }
single { single {
MessageItemFactory(get(), get(), get()) EventHtmlRenderer(homeActivity, get())
}
single {
MessageItemFactory(get(), get(), get(), get())
} }
single { single {

View File

@ -30,13 +30,13 @@ class HomeNavigator {
var activity: HomeActivity? = null var activity: HomeActivity? = null
private var currentRoomId: String? = null private var rootRoomId: String? = null
fun openRoomDetail(roomId: String, fun openRoomDetail(roomId: String,
eventId: String?, eventId: String?,
addToBackstack: Boolean = false) { addToBackstack: Boolean = false) {
Timber.v("Open room detail $roomId - $eventId - $addToBackstack") Timber.v("Open room detail $roomId - $eventId - $addToBackstack")
if (!addToBackstack && isRoomOpened(roomId)) { if (!addToBackstack && isRoot(roomId)) {
return return
} }
activity?.let { activity?.let {
@ -46,7 +46,7 @@ class HomeNavigator {
if (addToBackstack) { if (addToBackstack) {
it.addFragmentToBackstack(roomDetailFragment, R.id.homeDetailFragmentContainer, roomId) it.addFragmentToBackstack(roomDetailFragment, R.id.homeDetailFragmentContainer, roomId)
} else { } else {
currentRoomId = roomId rootRoomId = roomId
clearBackStack(it.supportFragmentManager) clearBackStack(it.supportFragmentManager)
it.replaceFragment(roomDetailFragment, R.id.homeDetailFragmentContainer) it.replaceFragment(roomDetailFragment, R.id.homeDetailFragmentContainer)
} }
@ -61,9 +61,7 @@ class HomeNavigator {
Timber.v("Open user detail $userId") Timber.v("Open user detail $userId")
} }
fun isRoomOpened(roomId: String): Boolean { // Private Methods *****************************************************************************
return currentRoomId == roomId
}
private fun clearBackStack(fragmentManager: FragmentManager) { private fun clearBackStack(fragmentManager: FragmentManager) {
if (fragmentManager.backStackEntryCount > 0) { if (fragmentManager.backStackEntryCount > 0) {
@ -72,4 +70,8 @@ class HomeNavigator {
} }
} }
private fun isRoot(roomId: String): Boolean {
return rootRoomId == roomId
}
} }

View File

@ -29,12 +29,14 @@ import im.vector.riotredesign.core.epoxy.RiotEpoxyModel
import im.vector.riotredesign.core.extensions.localDateTime import im.vector.riotredesign.core.extensions.localDateTime
import im.vector.riotredesign.core.resources.ColorProvider import im.vector.riotredesign.core.resources.ColorProvider
import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider import im.vector.riotredesign.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
import im.vector.riotredesign.features.html.EventHtmlRenderer
import im.vector.riotredesign.features.media.MediaContentRenderer import im.vector.riotredesign.features.media.MediaContentRenderer
import me.gujun.android.span.span import me.gujun.android.span.span
class MessageItemFactory(private val colorProvider: ColorProvider, class MessageItemFactory(private val colorProvider: ColorProvider,
private val timelineMediaSizeProvider: TimelineMediaSizeProvider, private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
private val timelineDateFormatter: TimelineDateFormatter) { private val timelineDateFormatter: TimelineDateFormatter,
private val htmlRenderer: EventHtmlRenderer) {
private val messagesDisplayedWithInformation = HashSet<String?>() private val messagesDisplayedWithInformation = HashSet<String?>()
@ -102,9 +104,15 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
informationData: MessageInformationData, informationData: MessageInformationData,
callback: TimelineEventController.Callback?): MessageTextItem? { callback: TimelineEventController.Callback?): MessageTextItem? {
val message = linkifyBody(messageContent.body, callback) val bodyToUse = messageContent.formattedBody
?.let {
htmlRenderer.render(it)
}
?: messageContent.body
val linkifiedBody = linkifyBody(bodyToUse, callback)
return MessageTextItem_() return MessageTextItem_()
.message(message) .message(linkifiedBody)
.informationData(informationData) .informationData(informationData)
} }

View File

@ -37,13 +37,14 @@ class TimelineEventController(private val roomId: String,
EpoxyAsyncUtil.getAsyncBackgroundHandler(), EpoxyAsyncUtil.getAsyncBackgroundHandler(),
EpoxyAsyncUtil.getAsyncBackgroundHandler() EpoxyAsyncUtil.getAsyncBackgroundHandler()
) { ) {
init {
setFilterDuplicates(true)
}
private var isLoadingForward: Boolean = false private var isLoadingForward: Boolean = false
private var isLoadingBackward: Boolean = false private var isLoadingBackward: Boolean = false
private var hasReachedEnd: Boolean = false private var hasReachedEnd: Boolean = true
init {
requestModelBuild()
}
var callback: Callback? = null var callback: Callback? = null

View File

@ -0,0 +1,183 @@
/*
*
* * Copyright 2019 New Vector Ltd
* *
* * Licensed under the Apache License, Version 2.0 (the "License");
* * you may not use this file except in compliance with the License.
* * You may obtain a copy of the License at
* *
* * http://www.apache.org/licenses/LICENSE-2.0
* *
* * Unless required by applicable law or agreed to in writing, software
* * distributed under the License is distributed on an "AS IS" BASIS,
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* * See the License for the specific language governing permissions and
* * limitations under the License.
*
*/
package im.vector.riotredesign.features.html
import android.content.Context
import android.text.style.ImageSpan
import im.vector.matrix.android.api.permalinks.PermalinkData
import im.vector.matrix.android.api.permalinks.PermalinkParser
import im.vector.matrix.android.api.session.Session
import org.commonmark.node.BlockQuote
import org.commonmark.node.HtmlBlock
import org.commonmark.node.HtmlInline
import org.commonmark.node.Node
import ru.noties.markwon.AbstractMarkwonPlugin
import ru.noties.markwon.Markwon
import ru.noties.markwon.MarkwonConfiguration
import ru.noties.markwon.MarkwonVisitor
import ru.noties.markwon.SpannableBuilder
import ru.noties.markwon.html.HtmlTag
import ru.noties.markwon.html.MarkwonHtmlParserImpl
import ru.noties.markwon.html.MarkwonHtmlRenderer
import ru.noties.markwon.html.TagHandler
import ru.noties.markwon.html.tag.BlockquoteHandler
import ru.noties.markwon.html.tag.EmphasisHandler
import ru.noties.markwon.html.tag.HeadingHandler
import ru.noties.markwon.html.tag.ImageHandler
import ru.noties.markwon.html.tag.LinkHandler
import ru.noties.markwon.html.tag.ListHandler
import ru.noties.markwon.html.tag.StrikeHandler
import ru.noties.markwon.html.tag.StrongEmphasisHandler
import ru.noties.markwon.html.tag.SubScriptHandler
import ru.noties.markwon.html.tag.SuperScriptHandler
import ru.noties.markwon.html.tag.UnderlineHandler
import java.util.Arrays.asList
class EventHtmlRenderer(private val context: Context,
private val session: Session) {
private val markwon = Markwon.builder(context)
.usePlugin(MatrixPlugin.create(context, session))
.build()
fun render(text: String): CharSequence {
return markwon.toMarkdown(text)
}
}
private class MatrixPlugin private constructor(private val context: Context,
private val session: Session) : AbstractMarkwonPlugin() {
override fun configureConfiguration(builder: MarkwonConfiguration.Builder) {
builder.htmlParser(MarkwonHtmlParserImpl.create())
}
override fun configureHtmlRenderer(builder: MarkwonHtmlRenderer.Builder) {
builder
.addHandler(
"img",
ImageHandler.create())
.addHandler(
"a",
MxLinkHandler(context, session))
.addHandler(
"blockquote",
BlockquoteHandler())
.addHandler(
"sub",
SubScriptHandler())
.addHandler(
"sup",
SuperScriptHandler())
.addHandler(
asList<String>("b", "strong"),
StrongEmphasisHandler())
.addHandler(
asList<String>("s", "del"),
StrikeHandler())
.addHandler(
asList<String>("u", "ins"),
UnderlineHandler())
.addHandler(
asList<String>("ul", "ol"),
ListHandler())
.addHandler(
asList<String>("i", "em", "cite", "dfn"),
EmphasisHandler())
.addHandler(
asList<String>("h1", "h2", "h3", "h4", "h5", "h6"),
HeadingHandler())
.addHandler("mx-reply",
MxReplyTagHandler())
}
override fun afterRender(node: Node, visitor: MarkwonVisitor) {
val configuration = visitor.configuration()
configuration.htmlRenderer().render(visitor, configuration.htmlParser())
}
override fun configureVisitor(builder: MarkwonVisitor.Builder) {
builder
.on(HtmlBlock::class.java) { visitor, htmlBlock -> visitHtml(visitor, htmlBlock.literal) }
.on(HtmlInline::class.java) { visitor, htmlInline -> visitHtml(visitor, htmlInline.literal) }
}
private fun visitHtml(visitor: MarkwonVisitor, html: String?) {
if (html != null) {
visitor.configuration().htmlParser().processFragment(visitor.builder(), html)
}
}
companion object {
fun create(context: Context, session: Session): MatrixPlugin {
return MatrixPlugin(context, session)
}
}
}
private class MxLinkHandler(private val context: Context, private val session: Session) : TagHandler() {
private val linkHandler = LinkHandler()
override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) {
val link = tag.attributes()["href"]
if (link != null) {
val permalinkData = PermalinkParser.parse(link)
when (permalinkData) {
is PermalinkData.UserLink -> {
val user = session.getUser(permalinkData.userId) ?: return
val drawable = PillDrawableFactory.create(context, permalinkData.userId, user)
val span = ImageSpan(drawable)
SpannableBuilder.setSpans(
visitor.builder(),
span,
tag.start(),
tag.end()
)
}
else -> linkHandler.handle(visitor, renderer, tag)
}
} else {
linkHandler.handle(visitor, renderer, tag)
}
}
}
private class MxReplyTagHandler : TagHandler() {
override fun handle(visitor: MarkwonVisitor, renderer: MarkwonHtmlRenderer, tag: HtmlTag) {
val configuration = visitor.configuration()
val factory = configuration.spansFactory().get(BlockQuote::class.java)
if (factory != null) {
SpannableBuilder.setSpans(
visitor.builder(),
factory.getSpans(configuration, visitor.renderProps()),
tag.start(),
tag.end()
)
val replyText = visitor.builder().removeFromEnd(tag.end())
visitor.builder().append("\n\n").append(replyText)
}
}
}

View File

@ -0,0 +1,59 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.riotredesign.features.html
import android.content.Context
import android.graphics.drawable.Drawable
import com.google.android.material.chip.ChipDrawable
import im.vector.matrix.android.api.session.user.model.User
import im.vector.riotredesign.R
import im.vector.riotredesign.features.home.AvatarRenderer
import java.lang.ref.WeakReference
object PillDrawableFactory {
fun create(context: Context, userId: String, user: User?): Drawable {
val textPadding = context.resources.getDimension(R.dimen.pill_text_padding)
val chipDrawable = ChipDrawable.createFromResource(context, R.xml.pill_view).apply {
setText(user?.displayName ?: userId)
textEndPadding = textPadding
textStartPadding = textPadding
setChipMinHeightResource(R.dimen.pill_min_height)
setChipIconSizeResource(R.dimen.pill_avatar_size)
setBounds(0, 0, intrinsicWidth, intrinsicHeight)
}
val avatarRendererCallback = AvatarRendererChipCallback(chipDrawable)
AvatarRenderer.load(context, user?.avatarUrl, user?.displayName, 80, avatarRendererCallback)
return chipDrawable
}
private class AvatarRendererChipCallback(chipDrawable: ChipDrawable) : AvatarRenderer.Callback {
private val weakChipDrawable = WeakReference<ChipDrawable>(chipDrawable)
override fun onDrawableUpdated(drawable: Drawable?) {
weakChipDrawable.get()?.apply {
chipIcon = drawable
setBounds(0, 0, intrinsicWidth, intrinsicHeight)
}
}
}
}

View File

@ -17,4 +17,9 @@
<color name="pale_grey_two">#ebedf8</color> <color name="pale_grey_two">#ebedf8</color>
<color name="brown_grey">#a5a5a5</color> <color name="brown_grey">#a5a5a5</color>
<color name="grey_lynch">#61708B</color> <color name="grey_lynch">#61708B</color>
<color name="vector_silver_color">#FFC7C7C7</color>
<color name="vector_dark_grey_color">#FF999999</color>
<color name="vector_fuchsia_color">#FFF56679</color>
</resources> </resources>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="pill_avatar_size">16dp</dimen>
<dimen name="pill_min_height">20dp</dimen>
<dimen name="pill_text_padding">4dp</dimen>
</resources>

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="Base.V1.Theme.Riot" parent="Theme.AppCompat.Light.NoActionBar"> <style name="Base.V1.Theme.Riot" parent="Theme.MaterialComponents.Light.NoActionBar.Bridge">
<item name="colorPrimary">@color/dark</item> <item name="colorPrimary">@color/dark</item>
<item name="colorPrimaryDark">@color/dark</item> <item name="colorPrimaryDark">@color/dark</item>
<item name="colorAccent">@color/pale_teal</item> <item name="colorAccent">@color/pale_teal</item>
@ -11,5 +11,4 @@
<style name="Base.Theme.Riot" parent="Base.V1.Theme.Riot" /> <style name="Base.Theme.Riot" parent="Base.V1.Theme.Riot" />
</resources> </resources>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<chip xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
style="@style/Widget.MaterialComponents.Chip.Entry"
android:checkable="false"
app:closeIcon="@null" />

View File

@ -22,6 +22,7 @@ allprojects {
google() google()
jcenter() jcenter()
maven { url 'https://jitpack.io' } maven { url 'https://jitpack.io' }
maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
} }
} }

View File

@ -21,12 +21,13 @@ import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.session.content.ContentUrlResolver import im.vector.matrix.android.api.session.content.ContentUrlResolver
import im.vector.matrix.android.api.session.group.GroupService import im.vector.matrix.android.api.session.group.GroupService
import im.vector.matrix.android.api.session.room.RoomService import im.vector.matrix.android.api.session.room.RoomService
import im.vector.matrix.android.api.session.user.UserService
/** /**
* This interface defines interactions with a session. * This interface defines interactions with a session.
* An instance of a session will be provided by the SDK. * An instance of a session will be provided by the SDK.
*/ */
interface Session : RoomService, GroupService { interface Session : RoomService, GroupService, UserService {
/** /**
* The params associated to the session * The params associated to the session

View File

@ -0,0 +1,35 @@
/*
*
* * Copyright 2019 New Vector Ltd
* *
* * Licensed under the Apache License, Version 2.0 (the "License");
* * you may not use this file except in compliance with the License.
* * You may obtain a copy of the License at
* *
* * http://www.apache.org/licenses/LICENSE-2.0
* *
* * Unless required by applicable law or agreed to in writing, software
* * distributed under the License is distributed on an "AS IS" BASIS,
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* * See the License for the specific language governing permissions and
* * limitations under the License.
*
*/
package im.vector.matrix.android.api.session.user
import im.vector.matrix.android.api.session.user.model.User
/**
* This interface defines methods to get users. It's implemented at the session level.
*/
interface UserService {
/**
* Get a user from a userId
* @param userId the userId to look for.
* @return a user with userId or null
*/
fun getUser(userId: String): User?
}

View File

@ -0,0 +1,29 @@
/*
*
* * Copyright 2019 New Vector Ltd
* *
* * Licensed under the Apache License, Version 2.0 (the "License");
* * you may not use this file except in compliance with the License.
* * You may obtain a copy of the License at
* *
* * http://www.apache.org/licenses/LICENSE-2.0
* *
* * Unless required by applicable law or agreed to in writing, software
* * distributed under the License is distributed on an "AS IS" BASIS,
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* * See the License for the specific language governing permissions and
* * limitations under the License.
*
*/
package im.vector.matrix.android.api.session.user.model
/**
* Data class which holds information about a user.
* It can be retrieved with [im.vector.matrix.android.api.session.user.UserService]
*/
data class User(
val userId: String,
val displayName: String? = null,
val avatarUrl: String? = null
)

View File

@ -16,11 +16,12 @@
package im.vector.matrix.android.internal.database package im.vector.matrix.android.internal.database
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import com.zhuinden.monarchy.Monarchy import com.zhuinden.monarchy.Monarchy
import io.realm.OrderedCollectionChangeSet
import io.realm.RealmObject import io.realm.RealmObject
import io.realm.RealmResults
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
internal interface LiveEntityObserver { internal interface LiveEntityObserver {
fun start() fun start()
@ -28,38 +29,41 @@ internal interface LiveEntityObserver {
} }
internal abstract class RealmLiveEntityObserver<T : RealmObject>(protected val monarchy: Monarchy) internal abstract class RealmLiveEntityObserver<T : RealmObject>(protected val monarchy: Monarchy)
: Observer<Monarchy.ManagedChangeSet<T>>, LiveEntityObserver { : LiveEntityObserver {
protected abstract val query: Monarchy.Query<T> protected abstract val query: Monarchy.Query<T>
private val isStarted = AtomicBoolean(false) private val isStarted = AtomicBoolean(false)
private val liveResults: LiveData<Monarchy.ManagedChangeSet<T>> by lazy { private lateinit var results: AtomicReference<RealmResults<T>>
monarchy.findAllManagedWithChanges(query)
}
override fun start() { override fun start() {
if (isStarted.compareAndSet(false, true)) { if (isStarted.compareAndSet(false, true)) {
liveResults.observeForever(this) monarchy.postToMonarchyThread {
val queryResults = query.createQuery(it).findAll()
queryResults.addChangeListener { t, changeSet ->
onChanged(t, changeSet)
}
results = AtomicReference(queryResults)
}
} }
} }
override fun dispose() { override fun dispose() {
if (isStarted.compareAndSet(true, false)) { if (isStarted.compareAndSet(true, false)) {
liveResults.removeObserver(this) monarchy.postToMonarchyThread {
results.getAndSet(null).removeAllChangeListeners()
}
} }
} }
// PRIVATE // PRIVATE
override fun onChanged(changeSet: Monarchy.ManagedChangeSet<T>?) { private fun onChanged(realmResults: RealmResults<T>, changeSet: OrderedCollectionChangeSet) {
if (changeSet == null) { val insertionIndexes = changeSet.insertions
return val updateIndexes = changeSet.changes
} val deletionIndexes = changeSet.deletions
val insertionIndexes = changeSet.orderedCollectionChangeSet.insertions val inserted = realmResults.filterIndexed { index, _ -> insertionIndexes.contains(index) }
val updateIndexes = changeSet.orderedCollectionChangeSet.changes val updated = realmResults.filterIndexed { index, _ -> updateIndexes.contains(index) }
val deletionIndexes = changeSet.orderedCollectionChangeSet.deletions val deleted = realmResults.filterIndexed { index, _ -> deletionIndexes.contains(index) }
val inserted = changeSet.realmResults.filterIndexed { index, _ -> insertionIndexes.contains(index) }
val updated = changeSet.realmResults.filterIndexed { index, _ -> updateIndexes.contains(index) }
val deleted = changeSet.realmResults.filterIndexed { index, _ -> deletionIndexes.contains(index) }
process(inserted, updated, deleted) process(inserted, updated, deleted)
} }

View File

@ -21,6 +21,7 @@ import im.vector.matrix.android.internal.database.mapper.toEntity
import im.vector.matrix.android.internal.database.mapper.updateWith import im.vector.matrix.android.internal.database.mapper.updateWith
import im.vector.matrix.android.internal.database.model.ChunkEntity import im.vector.matrix.android.internal.database.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.query.fastContains
internal fun RoomEntity.deleteOnCascade(chunkEntity: ChunkEntity) { internal fun RoomEntity.deleteOnCascade(chunkEntity: ChunkEntity) {
@ -37,16 +38,17 @@ internal fun RoomEntity.addOrUpdate(chunkEntity: ChunkEntity) {
internal fun RoomEntity.addStateEvents(stateEvents: List<Event>, internal fun RoomEntity.addStateEvents(stateEvents: List<Event>,
stateIndex: Int = Int.MIN_VALUE, stateIndex: Int = Int.MIN_VALUE,
filterDuplicates: Boolean = false,
isUnlinked: Boolean = false) { isUnlinked: Boolean = false) {
if (!isManaged) { if (!isManaged) {
throw IllegalStateException("Chunk entity should be managed to use fast contains") throw IllegalStateException("Chunk entity should be managed to use fast contains")
} }
stateEvents.forEach { event -> stateEvents.forEach { event ->
if (event.eventId == null) { if (event.eventId == null || (filterDuplicates && fastContains(event.eventId))) {
return@forEach return@forEach
} }
val eventEntity = event.toEntity(roomId) val eventEntity = event.toEntity(roomId)
eventEntity.updateWith(stateIndex, isUnlinked) eventEntity.updateWith(stateIndex, isUnlinked)
untimelinedStateEvents.add(eventEntity) untimelinedStateEvents.add(eventEntity)
} }
} }

View File

@ -0,0 +1,31 @@
/*
*
* * Copyright 2019 New Vector Ltd
* *
* * Licensed under the Apache License, Version 2.0 (the "License");
* * you may not use this file except in compliance with the License.
* * You may obtain a copy of the License at
* *
* * http://www.apache.org/licenses/LICENSE-2.0
* *
* * Unless required by applicable law or agreed to in writing, software
* * distributed under the License is distributed on an "AS IS" BASIS,
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* * See the License for the specific language governing permissions and
* * limitations under the License.
*
*/
package im.vector.matrix.android.internal.database.model
import io.realm.RealmObject
import io.realm.annotations.PrimaryKey
internal open class UserEntity(@PrimaryKey var userId: String = "",
var displayName: String = "",
var avatarUrl: String = ""
) : RealmObject() {
companion object
}

View File

@ -27,8 +27,7 @@ import io.realm.Sort
import io.realm.kotlin.where import io.realm.kotlin.where
internal fun EventEntity.Companion.where(realm: Realm, eventId: String): RealmQuery<EventEntity> { internal fun EventEntity.Companion.where(realm: Realm, eventId: String): RealmQuery<EventEntity> {
return realm.where<EventEntity>() return realm.where<EventEntity>().equalTo(EventEntityFields.EVENT_ID, eventId)
.equalTo(EventEntityFields.EVENT_ID, eventId)
} }
internal fun EventEntity.Companion.where(realm: Realm, internal fun EventEntity.Companion.where(realm: Realm,

View File

@ -17,6 +17,7 @@
package im.vector.matrix.android.internal.database.query package im.vector.matrix.android.internal.database.query
import im.vector.matrix.android.api.session.room.model.MyMembership import im.vector.matrix.android.api.session.room.model.MyMembership
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.RoomEntity import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.RoomEntityFields import im.vector.matrix.android.internal.database.model.RoomEntityFields
import io.realm.Realm import io.realm.Realm
@ -34,3 +35,7 @@ internal fun RoomEntity.Companion.where(realm: Realm, membership: MyMembership?
} }
return query return query
} }
internal fun RoomEntity.fastContains(eventId: String): Boolean {
return EventEntity.where(realm, eventId = eventId).findFirst() != null
}

View File

@ -0,0 +1,31 @@
/*
*
* * Copyright 2019 New Vector Ltd
* *
* * Licensed under the Apache License, Version 2.0 (the "License");
* * you may not use this file except in compliance with the License.
* * You may obtain a copy of the License at
* *
* * http://www.apache.org/licenses/LICENSE-2.0
* *
* * Unless required by applicable law or agreed to in writing, software
* * distributed under the License is distributed on an "AS IS" BASIS,
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* * See the License for the specific language governing permissions and
* * limitations under the License.
*
*/
package im.vector.matrix.android.internal.database.query
import im.vector.matrix.android.internal.database.model.UserEntity
import im.vector.matrix.android.internal.database.model.UserEntityFields
import io.realm.Realm
import io.realm.RealmQuery
import io.realm.kotlin.where
internal fun UserEntity.Companion.where(realm: Realm, userId: String): RealmQuery<UserEntity> {
return realm
.where<UserEntity>()
.equalTo(UserEntityFields.USER_ID, userId)
}

View File

@ -19,6 +19,7 @@ package im.vector.matrix.android.internal.session
import android.os.Looper import android.os.Looper
import androidx.annotation.MainThread import androidx.annotation.MainThread
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.auth.data.SessionParams import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.session.Session import im.vector.matrix.android.api.session.Session
import im.vector.matrix.android.api.session.content.ContentUrlResolver import im.vector.matrix.android.api.session.content.ContentUrlResolver
@ -28,6 +29,8 @@ import im.vector.matrix.android.api.session.group.model.GroupSummary
import im.vector.matrix.android.api.session.room.Room import im.vector.matrix.android.api.session.room.Room
import im.vector.matrix.android.api.session.room.RoomService import im.vector.matrix.android.api.session.room.RoomService
import im.vector.matrix.android.api.session.room.model.RoomSummary import im.vector.matrix.android.api.session.room.model.RoomSummary
import im.vector.matrix.android.api.session.user.UserService
import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.internal.database.LiveEntityObserver import im.vector.matrix.android.internal.database.LiveEntityObserver
import im.vector.matrix.android.internal.di.MatrixKoinComponent import im.vector.matrix.android.internal.di.MatrixKoinComponent
import im.vector.matrix.android.internal.di.MatrixKoinHolder import im.vector.matrix.android.internal.di.MatrixKoinHolder
@ -35,11 +38,12 @@ import im.vector.matrix.android.internal.session.group.GroupModule
import im.vector.matrix.android.internal.session.room.RoomModule import im.vector.matrix.android.internal.session.room.RoomModule
import im.vector.matrix.android.internal.session.sync.SyncModule import im.vector.matrix.android.internal.session.sync.SyncModule
import im.vector.matrix.android.internal.session.sync.job.SyncThread import im.vector.matrix.android.internal.session.sync.job.SyncThread
import im.vector.matrix.android.internal.session.user.UserModule
import org.koin.core.scope.Scope import org.koin.core.scope.Scope
import org.koin.standalone.inject import org.koin.standalone.inject
internal class DefaultSession(override val sessionParams: SessionParams) : Session, MatrixKoinComponent, RoomService { internal class DefaultSession(override val sessionParams: SessionParams) : Session, MatrixKoinComponent {
companion object { companion object {
const val SCOPE: String = "session" const val SCOPE: String = "session"
@ -47,10 +51,12 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi
private lateinit var scope: Scope private lateinit var scope: Scope
private val monarchy by inject<Monarchy>()
private val liveEntityUpdaters by inject<List<LiveEntityObserver>>() private val liveEntityUpdaters by inject<List<LiveEntityObserver>>()
private val sessionListeners by inject<SessionListeners>() private val sessionListeners by inject<SessionListeners>()
private val roomService by inject<RoomService>() private val roomService by inject<RoomService>()
private val groupService by inject<GroupService>() private val groupService by inject<GroupService>()
private val userService by inject<UserService>()
private val syncThread by inject<SyncThread>() private val syncThread by inject<SyncThread>()
private val contentUrlResolver by inject<ContentUrlResolver>() private val contentUrlResolver by inject<ContentUrlResolver>()
private var isOpen = false private var isOpen = false
@ -64,8 +70,12 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi
val syncModule = SyncModule().definition val syncModule = SyncModule().definition
val roomModule = RoomModule().definition val roomModule = RoomModule().definition
val groupModule = GroupModule().definition val groupModule = GroupModule().definition
MatrixKoinHolder.instance.loadModules(listOf(sessionModule, syncModule, roomModule, groupModule)) val userModule = UserModule().definition
MatrixKoinHolder.instance.loadModules(listOf(sessionModule, syncModule, roomModule, groupModule, userModule))
scope = getKoin().getOrCreateScope(SCOPE) scope = getKoin().getOrCreateScope(SCOPE)
if (!monarchy.isMonarchyThreadOpen) {
monarchy.openManually()
}
liveEntityUpdaters.forEach { it.start() } liveEntityUpdaters.forEach { it.start() }
syncThread.start() syncThread.start()
} }
@ -77,6 +87,9 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi
assert(isOpen) assert(isOpen)
syncThread.kill() syncThread.kill()
liveEntityUpdaters.forEach { it.dispose() } liveEntityUpdaters.forEach { it.dispose() }
if (monarchy.isMonarchyThreadOpen) {
monarchy.closeManually()
}
scope.close() scope.close()
isOpen = false isOpen = false
} }
@ -118,6 +131,13 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi
return groupService.liveGroupSummaries() return groupService.liveGroupSummaries()
} }
// USER SERVICE
override fun getUser(userId: String): User? {
assert(isOpen)
return userService.getUser(userId)
}
// Private methods ***************************************************************************** // Private methods *****************************************************************************
private fun assertMainThread() { private fun assertMainThread() {

View File

@ -22,17 +22,20 @@ import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.session.content.ContentUrlResolver import im.vector.matrix.android.api.session.content.ContentUrlResolver
import im.vector.matrix.android.api.session.group.GroupService import im.vector.matrix.android.api.session.group.GroupService
import im.vector.matrix.android.api.session.room.RoomService import im.vector.matrix.android.api.session.room.RoomService
import im.vector.matrix.android.api.session.user.UserService
import im.vector.matrix.android.internal.database.LiveEntityObserver import im.vector.matrix.android.internal.database.LiveEntityObserver
import im.vector.matrix.android.internal.session.content.DefaultContentUrlResolver import im.vector.matrix.android.internal.session.content.DefaultContentUrlResolver
import im.vector.matrix.android.internal.session.group.DefaultGroupService import im.vector.matrix.android.internal.session.group.DefaultGroupService
import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater import im.vector.matrix.android.internal.session.group.GroupSummaryUpdater
import im.vector.matrix.android.internal.session.room.DefaultRoomService import im.vector.matrix.android.internal.session.room.DefaultRoomService
import im.vector.matrix.android.internal.session.room.RoomAvatarResolver import im.vector.matrix.android.internal.session.room.RoomAvatarResolver
import im.vector.matrix.android.internal.session.room.RoomFactory
import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater import im.vector.matrix.android.internal.session.room.RoomSummaryUpdater
import im.vector.matrix.android.internal.session.room.members.RoomDisplayNameResolver import im.vector.matrix.android.internal.session.room.members.RoomDisplayNameResolver
import im.vector.matrix.android.internal.session.room.members.RoomMemberDisplayNameResolver import im.vector.matrix.android.internal.session.room.members.RoomMemberDisplayNameResolver
import im.vector.matrix.android.internal.session.room.prune.EventsPruner import im.vector.matrix.android.internal.session.room.prune.EventsPruner
import im.vector.matrix.android.internal.session.user.DefaultUserService
import im.vector.matrix.android.internal.session.user.UserEntityUpdater
import im.vector.matrix.android.internal.session.user.UserModule
import im.vector.matrix.android.internal.util.md5 import im.vector.matrix.android.internal.util.md5
import io.realm.RealmConfiguration import io.realm.RealmConfiguration
import org.koin.dsl.module.module import org.koin.dsl.module.module
@ -96,6 +99,10 @@ internal class SessionModule(private val sessionParams: SessionParams) {
DefaultGroupService(get()) as GroupService DefaultGroupService(get()) as GroupService
} }
scope(DefaultSession.SCOPE) {
DefaultUserService(get()) as UserService
}
scope(DefaultSession.SCOPE) { scope(DefaultSession.SCOPE) {
SessionListeners() SessionListeners()
} }
@ -108,7 +115,8 @@ internal class SessionModule(private val sessionParams: SessionParams) {
val roomSummaryUpdater = RoomSummaryUpdater(get(), get(), get(), get(), sessionParams.credentials) val roomSummaryUpdater = RoomSummaryUpdater(get(), get(), get(), get(), sessionParams.credentials)
val groupSummaryUpdater = GroupSummaryUpdater(get()) val groupSummaryUpdater = GroupSummaryUpdater(get())
val eventsPruner = EventsPruner(get()) val eventsPruner = EventsPruner(get())
listOf<LiveEntityObserver>(roomSummaryUpdater, groupSummaryUpdater, eventsPruner) val userEntityUpdater = UserEntityUpdater(get(), get(), get())
listOf<LiveEntityObserver>(roomSummaryUpdater, groupSummaryUpdater, eventsPruner, userEntityUpdater)
} }

View File

@ -34,7 +34,7 @@ import im.vector.matrix.android.internal.session.room.members.LoadRoomMembersTas
import im.vector.matrix.android.internal.task.TaskExecutor import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.configureWith import im.vector.matrix.android.internal.task.configureWith
internal data class DefaultRoom( internal class DefaultRoom(
override val roomId: String, override val roomId: String,
private val loadRoomMembersTask: LoadRoomMembersTask, private val loadRoomMembersTask: LoadRoomMembersTask,
private val monarchy: Monarchy, private val monarchy: Monarchy,

View File

@ -26,13 +26,13 @@ import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields import im.vector.matrix.android.internal.database.model.RoomSummaryEntityFields
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.util.fetchCopied import im.vector.matrix.android.internal.util.fetchManaged
internal class DefaultRoomService(private val monarchy: Monarchy, internal class DefaultRoomService(private val monarchy: Monarchy,
private val roomFactory: RoomFactory) : RoomService { private val roomFactory: RoomFactory) : RoomService {
override fun getRoom(roomId: String): Room? { override fun getRoom(roomId: String): Room? {
monarchy.fetchCopied { RoomEntity.where(it, roomId).findFirst() } ?: return null monarchy.fetchManaged { RoomEntity.where(it, roomId).findFirst() } ?: return null
return roomFactory.instantiate(roomId) return roomFactory.instantiate(roomId)
} }

View File

@ -34,22 +34,23 @@ internal class RoomMemberExtractor(private val monarchy: Monarchy,
private val cached = HashMap<String, RoomMember?>() private val cached = HashMap<String, RoomMember?>()
fun extractFrom(event: EventEntity): RoomMember? { fun extractFrom(event: EventEntity): RoomMember? {
if (cached.containsKey(event.eventId)) {
return cached[event.eventId]
}
val sender = event.sender ?: return null val sender = event.sender ?: return null
val cacheKey = sender + event.stateIndex
if (cached.containsKey(cacheKey)) {
return cached[cacheKey]
}
// If the event is unlinked we want to fetch unlinked state events // If the event is unlinked we want to fetch unlinked state events
val unlinked = event.isUnlinked val unlinked = event.isUnlinked
// When stateIndex is negative, we try to get the next stateEvent prevContent() // When stateIndex is negative, we try to get the next stateEvent prevContent()
// If prevContent is null we fallback to the Int.MIN state events content() // If prevContent is null we fallback to the Int.MIN state events content()
val content = if (event.stateIndex <= 0) { val content = if (event.stateIndex <= 0) {
baseQuery(monarchy, roomId, sender, unlinked).next(from = event.stateIndex)?.prevContent baseQuery(monarchy, roomId, sender, unlinked).next(from = event.stateIndex)?.prevContent
?: baseQuery(monarchy, roomId, sender, unlinked).last(since = event.stateIndex)?.content ?: baseQuery(monarchy, roomId, sender, unlinked).last(since = event.stateIndex)?.content
} else { } else {
baseQuery(monarchy, roomId, sender, unlinked).last(since = event.stateIndex)?.content baseQuery(monarchy, roomId, sender, unlinked).last(since = event.stateIndex)?.content
} }
val roomMember: RoomMember? = ContentMapper.map(content).toModel() val roomMember: RoomMember? = ContentMapper.map(content).toModel()
cached[event.eventId] = roomMember cached[cacheKey] = roomMember
return roomMember return roomMember
} }

View File

@ -26,6 +26,7 @@ import im.vector.matrix.android.internal.database.model.EventEntityFields
import im.vector.matrix.android.internal.database.model.RoomSummaryEntity import im.vector.matrix.android.internal.database.model.RoomSummaryEntity
import im.vector.matrix.android.internal.database.query.where import im.vector.matrix.android.internal.database.query.where
import io.realm.Realm import io.realm.Realm
import io.realm.Sort
internal class RoomMembers(private val realm: Realm, internal class RoomMembers(private val realm: Realm,
private val roomId: String private val roomId: String
@ -35,6 +36,17 @@ internal class RoomMembers(private val realm: Realm,
RoomSummaryEntity.where(realm, roomId).findFirst() RoomSummaryEntity.where(realm, roomId).findFirst()
} }
fun get(userId: String): RoomMember? {
return EventEntity
.where(realm, roomId, EventType.STATE_ROOM_MEMBER)
.sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING)
.equalTo(EventEntityFields.STATE_KEY, userId)
.findFirst()
?.let {
it.asDomain().content?.toModel<RoomMember>()
}
}
fun getLoaded(): Map<String, RoomMember> { fun getLoaded(): Map<String, RoomMember> {
return EventEntity return EventEntity
.where(realm, roomId, EventType.STATE_ROOM_MEMBER) .where(realm, roomId, EventType.STATE_ROOM_MEMBER)
@ -45,7 +57,6 @@ internal class RoomMembers(private val realm: Realm,
.mapValues { it.value.content.toModel<RoomMember>()!! } .mapValues { it.value.content.toModel<RoomMember>()!! }
} }
fun getNumberOfJoinedMembers(): Int { fun getNumberOfJoinedMembers(): Int {
return roomSummary?.joinedMembersCount return roomSummary?.joinedMembersCount
?: getLoaded().filterValues { it.membership == Membership.JOIN }.size ?: getLoaded().filterValues { it.membership == Membership.JOIN }.size

View File

@ -92,7 +92,7 @@ internal class RoomSyncHandler(private val monarchy: Monarchy,
if (roomSync.state != null && roomSync.state.events.isNotEmpty()) { if (roomSync.state != null && roomSync.state.events.isNotEmpty()) {
val untimelinedStateIndex = if (isInitialSync) Int.MIN_VALUE else stateIndexOffset val untimelinedStateIndex = if (isInitialSync) Int.MIN_VALUE else stateIndexOffset
roomEntity.addStateEvents(roomSync.state.events, stateIndex = untimelinedStateIndex) roomEntity.addStateEvents(roomSync.state.events, filterDuplicates = true, stateIndex = untimelinedStateIndex)
} }
if (roomSync.timeline != null && roomSync.timeline.events.isNotEmpty()) { if (roomSync.timeline != null && roomSync.timeline.events.isNotEmpty()) {

View File

@ -0,0 +1,40 @@
/*
*
* * Copyright 2019 New Vector Ltd
* *
* * Licensed under the Apache License, Version 2.0 (the "License");
* * you may not use this file except in compliance with the License.
* * You may obtain a copy of the License at
* *
* * http://www.apache.org/licenses/LICENSE-2.0
* *
* * Unless required by applicable law or agreed to in writing, software
* * distributed under the License is distributed on an "AS IS" BASIS,
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* * See the License for the specific language governing permissions and
* * limitations under the License.
*
*/
package im.vector.matrix.android.internal.session.user
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.user.UserService
import im.vector.matrix.android.api.session.user.model.User
import im.vector.matrix.android.internal.database.model.UserEntity
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.util.fetchCopied
internal class DefaultUserService(private val monarchy: Monarchy) : UserService {
override fun getUser(userId: String): User? {
val userEntity = monarchy.fetchCopied { UserEntity.where(it, userId).findFirst() }
?: return null
return User(
userEntity.userId,
userEntity.displayName,
userEntity.avatarUrl
)
}
}

View File

@ -0,0 +1,56 @@
/*
* Copyright 2019 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package im.vector.matrix.android.internal.session.user
import arrow.core.Try
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.room.model.Membership
import im.vector.matrix.android.internal.database.mapper.asDomain
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.UserEntity
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.session.room.members.RoomMembers
import im.vector.matrix.android.internal.task.Task
import im.vector.matrix.android.internal.util.tryTransactionSync
internal interface UpdateUserTask : Task<UpdateUserTask.Params, Unit> {
data class Params(val eventIds: List<String>)
}
internal class DefaultUpdateUserTask(private val monarchy: Monarchy) : UpdateUserTask {
override fun execute(params: UpdateUserTask.Params): Try<Unit> {
return monarchy.tryTransactionSync { realm ->
params.eventIds.forEach { eventId ->
val event = EventEntity.where(realm, eventId).findFirst()?.asDomain()
?: return@forEach
val roomId = event.roomId ?: return@forEach
val userId = event.stateKey ?: return@forEach
val roomMember = RoomMembers(realm, roomId).get(userId) ?: return@forEach
if (roomMember.membership != Membership.JOIN) return@forEach
val userEntity = UserEntity.where(realm, userId).findFirst()
?: realm.createObject(UserEntity::class.java, userId)
userEntity.displayName = roomMember.displayName ?: ""
userEntity.avatarUrl = roomMember.avatarUrl ?: ""
}
}
}
}

View File

@ -0,0 +1,53 @@
/*
*
* * Copyright 2019 New Vector Ltd
* *
* * Licensed under the Apache License, Version 2.0 (the "License");
* * you may not use this file except in compliance with the License.
* * You may obtain a copy of the License at
* *
* * http://www.apache.org/licenses/LICENSE-2.0
* *
* * Unless required by applicable law or agreed to in writing, software
* * distributed under the License is distributed on an "AS IS" BASIS,
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* * See the License for the specific language governing permissions and
* * limitations under the License.
*
*/
package im.vector.matrix.android.internal.session.user
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.session.events.model.EventType
import im.vector.matrix.android.internal.database.RealmLiveEntityObserver
import im.vector.matrix.android.internal.database.model.EventEntity
import im.vector.matrix.android.internal.database.model.EventEntityFields
import im.vector.matrix.android.internal.database.query.where
import im.vector.matrix.android.internal.task.TaskExecutor
import im.vector.matrix.android.internal.task.TaskThread
import im.vector.matrix.android.internal.task.configureWith
import io.realm.Sort
internal class UserEntityUpdater(monarchy: Monarchy,
private val updateUserTask: UpdateUserTask,
private val taskExecutor: TaskExecutor)
: RealmLiveEntityObserver<EventEntity>(monarchy) {
override val query = Monarchy.Query<EventEntity> {
EventEntity
.where(it, type = EventType.STATE_ROOM_MEMBER)
.sort(EventEntityFields.STATE_INDEX, Sort.DESCENDING)
.distinct(EventEntityFields.STATE_KEY)
}
override fun process(inserted: List<EventEntity>, updated: List<EventEntity>, deleted: List<EventEntity>) {
val roomMembersEvents = inserted.map { it.eventId }
val taskParams = UpdateUserTask.Params(roomMembersEvents)
updateUserTask
.configureWith(taskParams)
.executeOn(TaskThread.IO)
.executeBy(taskExecutor)
}
}

View File

@ -14,9 +14,18 @@
* limitations under the License. * limitations under the License.
*/ */
package im.vector.riotredesign.core.extensions package im.vector.matrix.android.internal.session.user
import im.vector.matrix.android.internal.session.DefaultSession
import org.koin.dsl.module.module
fun CharSequence.firstCharAsString(): String { internal class UserModule {
return if (isNotEmpty()) this[0].toString() else ""
} val definition = module(override = true) {
scope(DefaultSession.SCOPE) {
DefaultUpdateUserTask(get()) as UpdateUserTask
}
}
}

View File

@ -34,11 +34,25 @@ internal fun Monarchy.tryTransactionAsync(transaction: (realm: Realm) -> Unit):
} }
} }
fun <T : RealmModel> Monarchy.fetchManaged(query: (Realm) -> T?): T? {
return fetch(query, false)
}
fun <T : RealmModel> Monarchy.fetchCopied(query: (Realm) -> T?): T? { fun <T : RealmModel> Monarchy.fetchCopied(query: (Realm) -> T?): T? {
return fetch(query, true)
}
private fun <T : RealmModel> Monarchy.fetch(query: (Realm) -> T?, copyFromRealm: Boolean): T? {
val ref = AtomicReference<T>() val ref = AtomicReference<T>()
doWithRealm { realm -> doWithRealm { realm ->
val result = query.invoke(realm)?.let { realm.copyFromRealm(it) } val result = query.invoke(realm)?.let {
if (copyFromRealm) {
realm.copyFromRealm(it)
} else {
it
}
}
ref.set(result) ref.set(result)
} }
return ref.get() return ref.get()
} }