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 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-rx")
implementation 'com.android.support:multidex:1.0.3'
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.constraintlayout:constraintlayout:1.1.3'
@ -97,6 +101,9 @@ dependencies {
implementation 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
implementation 'com.google.android.material:material:1.1.0-alpha02'
implementation 'me.gujun.android:span:1.7'
implementation "ru.noties.markwon:core:$markwon_version"
implementation "ru.noties.markwon:html:$markwon_version"
// DI
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) {
supportFragmentManager.inTransaction { replace(frameId, fragment).addToBackStack(tag) }
}
fun AppCompatActivity.hideKeyboard() {
currentFocus?.hideKeyboard()
}

View file

@ -17,12 +17,9 @@
package im.vector.riotredesign.core.extensions
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.ZoneId
fun Event.localDateTime(): LocalDateTime {
val instant = Instant.ofEpochMilli(originServerTs ?: 0)
return LocalDateTime.ofInstant(instant, ZoneId.systemDefault())
return DateProvider.toLocalDateTime(originServerTs)
}

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
import android.content.Context
import android.graphics.drawable.Drawable
import android.widget.ImageView
import androidx.core.content.ContextCompat
import com.amulyakhare.textdrawable.TextDrawable
import com.bumptech.glide.request.RequestOptions
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.RoomSummary
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.GlideRequest
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
object AvatarRenderer {
@ -41,17 +46,47 @@ object AvatarRenderer {
if (name.isNullOrEmpty()) {
return
}
val resolvedUrl = Matrix.getInstance().currentSession.contentUrlResolver().resolveFullSize(avatarUrl)
val avatarColor = ContextCompat.getColor(imageView.context, R.color.pale_teal)
val fallbackDrawable = TextDrawable.builder().buildRound(name.firstCharAsString().toUpperCase(), avatarColor)
GlideApp
.with(imageView)
.load(resolvedUrl)
.placeholder(fallbackDrawable)
.apply(RequestOptions.circleCropTransform())
val placeholder = buildPlaceholderDrawable(imageView.context, name)
buildGlideRequest(imageView.context, avatarUrl)
.placeholder(placeholder)
.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.widget.Toolbar
import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout
import androidx.fragment.app.FragmentManager
import com.airbnb.mvrx.viewModel
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.replaceFragment
import im.vector.riotredesign.core.platform.OnBackPressed
@ -44,11 +46,18 @@ class HomeActivity : RiotActivity(), ToolbarConfigurable {
private val homeActivityViewModel: HomeActivityViewModel by viewModel()
private val homeNavigator by inject<HomeNavigator>()
private val drawerListener = object : DrawerLayout.SimpleDrawerListener() {
override fun onDrawerStateChanged(newState: Int) {
hideKeyboard()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
loadKoinModules(listOf(HomeModule().definition))
loadKoinModules(listOf(HomeModule(this).definition))
homeNavigator.activity = this
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)
drawerLayout.addDrawerListener(drawerListener)
if (savedInstanceState == null) {
val homeDrawerFragment = HomeDrawerFragment.newInstance()
val loadingDetail = LoadingRoomDetailFragment.newInstance()
@ -61,6 +70,7 @@ class HomeActivity : RiotActivity(), ToolbarConfigurable {
}
override fun onDestroy() {
drawerLayout.removeDrawerListener(drawerListener)
homeNavigator.activity = null
super.onDestroy()
}

View file

@ -16,24 +16,43 @@
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.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.list.RoomSummaryComparator
import im.vector.riotredesign.features.home.room.list.RoomSummaryController
import im.vector.riotredesign.features.html.EventHtmlRenderer
import org.koin.dsl.module.module
class HomeModule {
class HomeModule(homeActivity: HomeActivity) {
val definition = module(override = true) {
single {
Matrix.getInstance().currentSession
}
single {
TimelineDateFormatter(get())
}
single {
MessageItemFactory(get(), get(), get())
EventHtmlRenderer(homeActivity, get())
}
single {
MessageItemFactory(get(), get(), get(), get())
}
single {

View file

@ -30,13 +30,13 @@ class HomeNavigator {
var activity: HomeActivity? = null
private var currentRoomId: String? = null
private var rootRoomId: String? = null
fun openRoomDetail(roomId: String,
eventId: String?,
addToBackstack: Boolean = false) {
Timber.v("Open room detail $roomId - $eventId - $addToBackstack")
if (!addToBackstack && isRoomOpened(roomId)) {
if (!addToBackstack && isRoot(roomId)) {
return
}
activity?.let {
@ -46,7 +46,7 @@ class HomeNavigator {
if (addToBackstack) {
it.addFragmentToBackstack(roomDetailFragment, R.id.homeDetailFragmentContainer, roomId)
} else {
currentRoomId = roomId
rootRoomId = roomId
clearBackStack(it.supportFragmentManager)
it.replaceFragment(roomDetailFragment, R.id.homeDetailFragmentContainer)
}
@ -61,9 +61,7 @@ class HomeNavigator {
Timber.v("Open user detail $userId")
}
fun isRoomOpened(roomId: String): Boolean {
return currentRoomId == roomId
}
// Private Methods *****************************************************************************
private fun clearBackStack(fragmentManager: FragmentManager) {
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.resources.ColorProvider
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 me.gujun.android.span.span
class MessageItemFactory(private val colorProvider: ColorProvider,
private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
private val timelineDateFormatter: TimelineDateFormatter) {
private val timelineDateFormatter: TimelineDateFormatter,
private val htmlRenderer: EventHtmlRenderer) {
private val messagesDisplayedWithInformation = HashSet<String?>()
@ -102,9 +104,15 @@ class MessageItemFactory(private val colorProvider: ColorProvider,
informationData: MessageInformationData,
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_()
.message(message)
.message(linkifiedBody)
.informationData(informationData)
}

View file

@ -37,13 +37,14 @@ class TimelineEventController(private val roomId: String,
EpoxyAsyncUtil.getAsyncBackgroundHandler(),
EpoxyAsyncUtil.getAsyncBackgroundHandler()
) {
init {
setFilterDuplicates(true)
}
private var isLoadingForward: Boolean = false
private var isLoadingBackward: Boolean = false
private var hasReachedEnd: Boolean = false
private var hasReachedEnd: Boolean = true
init {
requestModelBuild()
}
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="brown_grey">#a5a5a5</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>

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"?>
<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="colorPrimaryDark">@color/dark</item>
<item name="colorAccent">@color/pale_teal</item>
@ -11,5 +11,4 @@
<style name="Base.Theme.Riot" parent="Base.V1.Theme.Riot" />
</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()
jcenter()
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.group.GroupService
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.
* 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

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
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer
import com.zhuinden.monarchy.Monarchy
import io.realm.OrderedCollectionChangeSet
import io.realm.RealmObject
import io.realm.RealmResults
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicReference
internal interface LiveEntityObserver {
fun start()
@ -28,38 +29,41 @@ internal interface LiveEntityObserver {
}
internal abstract class RealmLiveEntityObserver<T : RealmObject>(protected val monarchy: Monarchy)
: Observer<Monarchy.ManagedChangeSet<T>>, LiveEntityObserver {
: LiveEntityObserver {
protected abstract val query: Monarchy.Query<T>
private val isStarted = AtomicBoolean(false)
private val liveResults: LiveData<Monarchy.ManagedChangeSet<T>> by lazy {
monarchy.findAllManagedWithChanges(query)
}
private lateinit var results: AtomicReference<RealmResults<T>>
override fun start() {
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() {
if (isStarted.compareAndSet(true, false)) {
liveResults.removeObserver(this)
monarchy.postToMonarchyThread {
results.getAndSet(null).removeAllChangeListeners()
}
}
}
// PRIVATE
override fun onChanged(changeSet: Monarchy.ManagedChangeSet<T>?) {
if (changeSet == null) {
return
}
val insertionIndexes = changeSet.orderedCollectionChangeSet.insertions
val updateIndexes = changeSet.orderedCollectionChangeSet.changes
val deletionIndexes = changeSet.orderedCollectionChangeSet.deletions
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) }
private fun onChanged(realmResults: RealmResults<T>, changeSet: OrderedCollectionChangeSet) {
val insertionIndexes = changeSet.insertions
val updateIndexes = changeSet.changes
val deletionIndexes = changeSet.deletions
val inserted = realmResults.filterIndexed { index, _ -> insertionIndexes.contains(index) }
val updated = realmResults.filterIndexed { index, _ -> updateIndexes.contains(index) }
val deleted = realmResults.filterIndexed { index, _ -> deletionIndexes.contains(index) }
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.model.ChunkEntity
import im.vector.matrix.android.internal.database.model.RoomEntity
import im.vector.matrix.android.internal.database.query.fastContains
internal fun RoomEntity.deleteOnCascade(chunkEntity: ChunkEntity) {
@ -37,16 +38,17 @@ internal fun RoomEntity.addOrUpdate(chunkEntity: ChunkEntity) {
internal fun RoomEntity.addStateEvents(stateEvents: List<Event>,
stateIndex: Int = Int.MIN_VALUE,
filterDuplicates: Boolean = false,
isUnlinked: Boolean = false) {
if (!isManaged) {
throw IllegalStateException("Chunk entity should be managed to use fast contains")
}
stateEvents.forEach { event ->
if (event.eventId == null) {
if (event.eventId == null || (filterDuplicates && fastContains(event.eventId))) {
return@forEach
}
val eventEntity = event.toEntity(roomId)
eventEntity.updateWith(stateIndex, isUnlinked)
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
internal fun EventEntity.Companion.where(realm: Realm, eventId: String): RealmQuery<EventEntity> {
return realm.where<EventEntity>()
.equalTo(EventEntityFields.EVENT_ID, eventId)
return realm.where<EventEntity>().equalTo(EventEntityFields.EVENT_ID, eventId)
}
internal fun EventEntity.Companion.where(realm: Realm,

View file

@ -17,6 +17,7 @@
package im.vector.matrix.android.internal.database.query
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.RoomEntityFields
import io.realm.Realm
@ -34,3 +35,7 @@ internal fun RoomEntity.Companion.where(realm: Realm, membership: MyMembership?
}
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 androidx.annotation.MainThread
import androidx.lifecycle.LiveData
import com.zhuinden.monarchy.Monarchy
import im.vector.matrix.android.api.auth.data.SessionParams
import im.vector.matrix.android.api.session.Session
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.RoomService
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.di.MatrixKoinComponent
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.sync.SyncModule
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.standalone.inject
internal class DefaultSession(override val sessionParams: SessionParams) : Session, MatrixKoinComponent, RoomService {
internal class DefaultSession(override val sessionParams: SessionParams) : Session, MatrixKoinComponent {
companion object {
const val SCOPE: String = "session"
@ -47,10 +51,12 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi
private lateinit var scope: Scope
private val monarchy by inject<Monarchy>()
private val liveEntityUpdaters by inject<List<LiveEntityObserver>>()
private val sessionListeners by inject<SessionListeners>()
private val roomService by inject<RoomService>()
private val groupService by inject<GroupService>()
private val userService by inject<UserService>()
private val syncThread by inject<SyncThread>()
private val contentUrlResolver by inject<ContentUrlResolver>()
private var isOpen = false
@ -64,8 +70,12 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi
val syncModule = SyncModule().definition
val roomModule = RoomModule().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)
if (!monarchy.isMonarchyThreadOpen) {
monarchy.openManually()
}
liveEntityUpdaters.forEach { it.start() }
syncThread.start()
}
@ -77,6 +87,9 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi
assert(isOpen)
syncThread.kill()
liveEntityUpdaters.forEach { it.dispose() }
if (monarchy.isMonarchyThreadOpen) {
monarchy.closeManually()
}
scope.close()
isOpen = false
}
@ -118,6 +131,13 @@ internal class DefaultSession(override val sessionParams: SessionParams) : Sessi
return groupService.liveGroupSummaries()
}
// USER SERVICE
override fun getUser(userId: String): User? {
assert(isOpen)
return userService.getUser(userId)
}
// Private methods *****************************************************************************
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.group.GroupService
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.session.content.DefaultContentUrlResolver
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.room.DefaultRoomService
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.members.RoomDisplayNameResolver
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.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 io.realm.RealmConfiguration
import org.koin.dsl.module.module
@ -96,6 +99,10 @@ internal class SessionModule(private val sessionParams: SessionParams) {
DefaultGroupService(get()) as GroupService
}
scope(DefaultSession.SCOPE) {
DefaultUserService(get()) as UserService
}
scope(DefaultSession.SCOPE) {
SessionListeners()
}
@ -108,7 +115,8 @@ internal class SessionModule(private val sessionParams: SessionParams) {
val roomSummaryUpdater = RoomSummaryUpdater(get(), get(), get(), get(), sessionParams.credentials)
val groupSummaryUpdater = GroupSummaryUpdater(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.configureWith
internal data class DefaultRoom(
internal class DefaultRoom(
override val roomId: String,
private val loadRoomMembersTask: LoadRoomMembersTask,
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.RoomSummaryEntityFields
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,
private val roomFactory: RoomFactory) : RoomService {
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)
}

View file

@ -34,22 +34,23 @@ internal class RoomMemberExtractor(private val monarchy: Monarchy,
private val cached = HashMap<String, RoomMember?>()
fun extractFrom(event: EventEntity): RoomMember? {
if (cached.containsKey(event.eventId)) {
return cached[event.eventId]
}
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
val unlinked = event.isUnlinked
// 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()
val content = if (event.stateIndex <= 0) {
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 {
baseQuery(monarchy, roomId, sender, unlinked).last(since = event.stateIndex)?.content
}
val roomMember: RoomMember? = ContentMapper.map(content).toModel()
cached[event.eventId] = roomMember
cached[cacheKey] = 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.query.where
import io.realm.Realm
import io.realm.Sort
internal class RoomMembers(private val realm: Realm,
private val roomId: String
@ -35,6 +36,17 @@ internal class RoomMembers(private val realm: Realm,
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> {
return EventEntity
.where(realm, roomId, EventType.STATE_ROOM_MEMBER)
@ -45,7 +57,6 @@ internal class RoomMembers(private val realm: Realm,
.mapValues { it.value.content.toModel<RoomMember>()!! }
}
fun getNumberOfJoinedMembers(): Int {
return roomSummary?.joinedMembersCount
?: 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()) {
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()) {

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.
*/
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 {
return if (isNotEmpty()) this[0].toString() else ""
}
internal class UserModule {
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? {
return fetch(query, true)
}
private fun <T : RealmModel> Monarchy.fetch(query: (Realm) -> T?, copyFromRealm: Boolean): T? {
val ref = AtomicReference<T>()
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)
}
return ref.get()
}
}