mirror of
https://github.com/oxen-io/session-android.git
synced 2025-01-11 01:43:39 +00:00
Merge remote-tracking branch 'upstream/dev' into SES-2009-blinded-conversation
This commit is contained in:
commit
f16735d4ee
@ -50,7 +50,7 @@ public class AttachmentServer implements Runnable {
|
|||||||
throws IOException
|
throws IOException
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
this.context = context;
|
this.context = context.getApplicationContext();
|
||||||
this.attachment = attachment;
|
this.attachment = attachment;
|
||||||
this.socket = new ServerSocket(0, 0, InetAddress.getByAddress(new byte[]{127, 0, 0, 1}));
|
this.socket = new ServerSocket(0, 0, InetAddress.getByAddress(new byte[]{127, 0, 0, 1}));
|
||||||
this.port = socket.getLocalPort();
|
this.port = socket.getLocalPort();
|
||||||
|
@ -122,7 +122,7 @@ class ProfilePictureView @JvmOverloads constructor(
|
|||||||
|
|
||||||
glide.clear(imageView)
|
glide.clear(imageView)
|
||||||
|
|
||||||
val placeholder = PlaceholderAvatarPhoto(context, publicKey, displayName ?: "${publicKey.take(4)}...${publicKey.takeLast(4)}")
|
val placeholder = PlaceholderAvatarPhoto(publicKey, displayName ?: "${publicKey.take(4)}...${publicKey.takeLast(4)}")
|
||||||
|
|
||||||
if (signalProfilePicture != null && avatar != "0" && avatar != "") {
|
if (signalProfilePicture != null && avatar != "0" && avatar != "") {
|
||||||
glide.load(signalProfilePicture)
|
glide.load(signalProfilePicture)
|
||||||
|
@ -287,8 +287,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
if (hexEncodedSeed == null) {
|
if (hexEncodedSeed == null) {
|
||||||
hexEncodedSeed = IdentityKeyUtil.getIdentityKeyPair(this).hexEncodedPrivateKey // Legacy account
|
hexEncodedSeed = IdentityKeyUtil.getIdentityKeyPair(this).hexEncodedPrivateKey // Legacy account
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val appContext = applicationContext
|
||||||
val loadFileContents: (String) -> String = { fileName ->
|
val loadFileContents: (String) -> String = { fileName ->
|
||||||
MnemonicUtilities.loadFileContents(this, fileName)
|
MnemonicUtilities.loadFileContents(appContext, fileName)
|
||||||
}
|
}
|
||||||
MnemonicCodec(loadFileContents).encode(hexEncodedSeed!!, MnemonicCodec.Language.Configuration.english)
|
MnemonicCodec(loadFileContents).encode(hexEncodedSeed!!, MnemonicCodec.Language.Configuration.english)
|
||||||
}
|
}
|
||||||
@ -831,6 +833,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
viewModel.saveDraft(binding?.inputBar?.text?.trim() ?: "")
|
viewModel.saveDraft(binding?.inputBar?.text?.trim() ?: "")
|
||||||
|
cancelVoiceMessage()
|
||||||
tearDownRecipientObserver()
|
tearDownRecipientObserver()
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
binding = null
|
binding = null
|
||||||
@ -1019,7 +1022,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun showVoiceMessageUI() {
|
override fun showVoiceMessageUI() {
|
||||||
binding?.inputBarRecordingView?.show()
|
binding?.inputBarRecordingView?.show(lifecycleScope)
|
||||||
binding?.inputBar?.alpha = 0.0f
|
binding?.inputBar?.alpha = 0.0f
|
||||||
val animation = ValueAnimator.ofObject(FloatEvaluator(), 1.0f, 0.0f)
|
val animation = ValueAnimator.ofObject(FloatEvaluator(), 1.0f, 0.0f)
|
||||||
animation.duration = 250L
|
animation.duration = 250L
|
||||||
|
@ -4,8 +4,6 @@ import android.animation.FloatEvaluator
|
|||||||
import android.animation.IntEvaluator
|
import android.animation.IntEvaluator
|
||||||
import android.animation.ValueAnimator
|
import android.animation.ValueAnimator
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
@ -14,6 +12,11 @@ import android.widget.RelativeLayout
|
|||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import androidx.core.content.res.ResourcesCompat
|
import androidx.core.content.res.ResourcesCompat
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.ViewInputBarRecordingBinding
|
import network.loki.messenger.databinding.ViewInputBarRecordingBinding
|
||||||
import org.thoughtcrime.securesms.util.DateUtils
|
import org.thoughtcrime.securesms.util.DateUtils
|
||||||
@ -25,10 +28,10 @@ import java.util.Date
|
|||||||
class InputBarRecordingView : RelativeLayout {
|
class InputBarRecordingView : RelativeLayout {
|
||||||
private lateinit var binding: ViewInputBarRecordingBinding
|
private lateinit var binding: ViewInputBarRecordingBinding
|
||||||
private var startTimestamp = 0L
|
private var startTimestamp = 0L
|
||||||
private val snHandler = Handler(Looper.getMainLooper())
|
|
||||||
private var dotViewAnimation: ValueAnimator? = null
|
private var dotViewAnimation: ValueAnimator? = null
|
||||||
private var pulseAnimation: ValueAnimator? = null
|
private var pulseAnimation: ValueAnimator? = null
|
||||||
var delegate: InputBarRecordingViewDelegate? = null
|
var delegate: InputBarRecordingViewDelegate? = null
|
||||||
|
private var timerJob: Job? = null
|
||||||
|
|
||||||
val lockView: LinearLayout
|
val lockView: LinearLayout
|
||||||
get() = binding.lockView
|
get() = binding.lockView
|
||||||
@ -50,9 +53,10 @@ class InputBarRecordingView : RelativeLayout {
|
|||||||
binding = ViewInputBarRecordingBinding.inflate(LayoutInflater.from(context), this, true)
|
binding = ViewInputBarRecordingBinding.inflate(LayoutInflater.from(context), this, true)
|
||||||
binding.inputBarMiddleContentContainer.disableClipping()
|
binding.inputBarMiddleContentContainer.disableClipping()
|
||||||
binding.inputBarCancelButton.setOnClickListener { hide() }
|
binding.inputBarCancelButton.setOnClickListener { hide() }
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun show() {
|
fun show(scope: CoroutineScope) {
|
||||||
startTimestamp = Date().time
|
startTimestamp = Date().time
|
||||||
binding.recordButtonOverlayImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_microphone, context.theme))
|
binding.recordButtonOverlayImageView.setImageDrawable(ResourcesCompat.getDrawable(resources, R.drawable.ic_microphone, context.theme))
|
||||||
binding.inputBarCancelButton.alpha = 0.0f
|
binding.inputBarCancelButton.alpha = 0.0f
|
||||||
@ -69,7 +73,7 @@ class InputBarRecordingView : RelativeLayout {
|
|||||||
animateDotView()
|
animateDotView()
|
||||||
pulse()
|
pulse()
|
||||||
animateLockViewUp()
|
animateLockViewUp()
|
||||||
updateTimer()
|
startTimer(scope)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hide() {
|
fun hide() {
|
||||||
@ -86,6 +90,24 @@ class InputBarRecordingView : RelativeLayout {
|
|||||||
}
|
}
|
||||||
animation.start()
|
animation.start()
|
||||||
delegate?.handleVoiceMessageUIHidden()
|
delegate?.handleVoiceMessageUIHidden()
|
||||||
|
stopTimer()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startTimer(scope: CoroutineScope) {
|
||||||
|
timerJob?.cancel()
|
||||||
|
timerJob = scope.launch {
|
||||||
|
while (isActive) {
|
||||||
|
val duration = (Date().time - startTimestamp) / 1000L
|
||||||
|
binding.recordingViewDurationTextView.text = DateUtils.formatElapsedTime(duration)
|
||||||
|
|
||||||
|
delay(500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopTimer() {
|
||||||
|
timerJob?.cancel()
|
||||||
|
timerJob = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun animateDotView() {
|
private fun animateDotView() {
|
||||||
@ -129,12 +151,6 @@ class InputBarRecordingView : RelativeLayout {
|
|||||||
animation.start()
|
animation.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateTimer() {
|
|
||||||
val duration = (Date().time - startTimestamp) / 1000L
|
|
||||||
binding.recordingViewDurationTextView.text = DateUtils.formatElapsedTime(duration)
|
|
||||||
snHandler.postDelayed({ updateTimer() }, 500)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun lock() {
|
fun lock() {
|
||||||
val fadeOutAnimation = ValueAnimator.ofObject(FloatEvaluator(), 1.0f, 0.0f)
|
val fadeOutAnimation = ValueAnimator.ofObject(FloatEvaluator(), 1.0f, 0.0f)
|
||||||
fadeOutAnimation.duration = 250L
|
fadeOutAnimation.duration = 250L
|
||||||
|
@ -881,6 +881,10 @@ public class ThreadDatabase extends Database {
|
|||||||
this.cursor = cursor;
|
this.cursor = cursor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getCount() {
|
||||||
|
return cursor == null ? 0 : cursor.getCount();
|
||||||
|
}
|
||||||
|
|
||||||
public ThreadRecord getNext() {
|
public ThreadRecord getNext() {
|
||||||
if (cursor == null || !cursor.moveToNext())
|
if (cursor == null || !cursor.moveToNext())
|
||||||
return null;
|
return null;
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package org.thoughtcrime.securesms.glide
|
package org.thoughtcrime.securesms.glide
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.graphics.drawable.BitmapDrawable
|
import android.graphics.drawable.BitmapDrawable
|
||||||
import com.bumptech.glide.load.Options
|
import com.bumptech.glide.load.Options
|
||||||
import com.bumptech.glide.load.model.ModelLoader
|
import com.bumptech.glide.load.model.ModelLoader
|
||||||
@ -8,7 +9,7 @@ import com.bumptech.glide.load.model.ModelLoaderFactory
|
|||||||
import com.bumptech.glide.load.model.MultiModelLoaderFactory
|
import com.bumptech.glide.load.model.MultiModelLoaderFactory
|
||||||
import org.session.libsession.avatars.PlaceholderAvatarPhoto
|
import org.session.libsession.avatars.PlaceholderAvatarPhoto
|
||||||
|
|
||||||
class PlaceholderAvatarLoader(): ModelLoader<PlaceholderAvatarPhoto, BitmapDrawable> {
|
class PlaceholderAvatarLoader(private val appContext: Context): ModelLoader<PlaceholderAvatarPhoto, BitmapDrawable> {
|
||||||
|
|
||||||
override fun buildLoadData(
|
override fun buildLoadData(
|
||||||
model: PlaceholderAvatarPhoto,
|
model: PlaceholderAvatarPhoto,
|
||||||
@ -16,14 +17,14 @@ class PlaceholderAvatarLoader(): ModelLoader<PlaceholderAvatarPhoto, BitmapDrawa
|
|||||||
height: Int,
|
height: Int,
|
||||||
options: Options
|
options: Options
|
||||||
): LoadData<BitmapDrawable> {
|
): LoadData<BitmapDrawable> {
|
||||||
return LoadData(model, PlaceholderAvatarFetcher(model.context, model))
|
return LoadData(model, PlaceholderAvatarFetcher(appContext, model))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun handles(model: PlaceholderAvatarPhoto): Boolean = true
|
override fun handles(model: PlaceholderAvatarPhoto): Boolean = true
|
||||||
|
|
||||||
class Factory() : ModelLoaderFactory<PlaceholderAvatarPhoto, BitmapDrawable> {
|
class Factory(private val appContext: Context) : ModelLoaderFactory<PlaceholderAvatarPhoto, BitmapDrawable> {
|
||||||
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<PlaceholderAvatarPhoto, BitmapDrawable> {
|
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<PlaceholderAvatarPhoto, BitmapDrawable> {
|
||||||
return PlaceholderAvatarLoader()
|
return PlaceholderAvatarLoader(appContext)
|
||||||
}
|
}
|
||||||
override fun teardown() {}
|
override fun teardown() {}
|
||||||
}
|
}
|
||||||
|
@ -2,12 +2,9 @@ package org.thoughtcrime.securesms.home
|
|||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.app.NotificationManager
|
import android.app.NotificationManager
|
||||||
import android.content.BroadcastReceiver
|
|
||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.SpannableString
|
import android.text.SpannableString
|
||||||
@ -18,13 +15,14 @@ import androidx.core.view.isVisible
|
|||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
@ -81,7 +79,6 @@ import org.thoughtcrime.securesms.util.IP2Country
|
|||||||
import org.thoughtcrime.securesms.util.disableClipping
|
import org.thoughtcrime.securesms.util.disableClipping
|
||||||
import org.thoughtcrime.securesms.util.push
|
import org.thoughtcrime.securesms.util.push
|
||||||
import org.thoughtcrime.securesms.util.show
|
import org.thoughtcrime.securesms.util.show
|
||||||
import org.thoughtcrime.securesms.util.themeState
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@ -99,7 +96,6 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
|
|
||||||
private lateinit var binding: ActivityHomeBinding
|
private lateinit var binding: ActivityHomeBinding
|
||||||
private lateinit var glide: GlideRequests
|
private lateinit var glide: GlideRequests
|
||||||
private var broadcastReceiver: BroadcastReceiver? = null
|
|
||||||
|
|
||||||
@Inject lateinit var threadDb: ThreadDatabase
|
@Inject lateinit var threadDb: ThreadDatabase
|
||||||
@Inject lateinit var mmsSmsDatabase: MmsSmsDatabase
|
@Inject lateinit var mmsSmsDatabase: MmsSmsDatabase
|
||||||
@ -205,18 +201,10 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
// Set up empty state view
|
// Set up empty state view
|
||||||
binding.createNewPrivateChatButton.setOnClickListener { showNewConversation() }
|
binding.createNewPrivateChatButton.setOnClickListener { showNewConversation() }
|
||||||
IP2Country.configureIfNeeded(this@HomeActivity)
|
IP2Country.configureIfNeeded(this@HomeActivity)
|
||||||
startObservingUpdates()
|
|
||||||
|
|
||||||
// Set up new conversation button
|
// Set up new conversation button
|
||||||
binding.newConversationButton.setOnClickListener { showNewConversation() }
|
binding.newConversationButton.setOnClickListener { showNewConversation() }
|
||||||
// Observe blocked contacts changed events
|
// Observe blocked contacts changed events
|
||||||
val broadcastReceiver = object : BroadcastReceiver() {
|
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
|
||||||
binding.recyclerView.adapter!!.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.broadcastReceiver = broadcastReceiver
|
|
||||||
LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, IntentFilter("blockedContactsChanged"))
|
|
||||||
|
|
||||||
// subscribe to outdated config updates, this should be removed after long enough time for device migration
|
// subscribe to outdated config updates, this should be removed after long enough time for device migration
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
@ -227,6 +215,27 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Subscribe to threads and update the UI
|
||||||
|
lifecycleScope.launch {
|
||||||
|
repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
homeViewModel.threads
|
||||||
|
.filterNotNull() // We don't actually want the null value here as it indicates a loading state (maybe we need a loading state?)
|
||||||
|
.collectLatest { threads ->
|
||||||
|
val manager = binding.recyclerView.layoutManager as LinearLayoutManager
|
||||||
|
val firstPos = manager.findFirstCompletelyVisibleItemPosition()
|
||||||
|
val offsetTop = if(firstPos >= 0) {
|
||||||
|
manager.findViewByPosition(firstPos)?.let { view ->
|
||||||
|
manager.getDecoratedTop(view) - manager.getTopDecorationHeight(view)
|
||||||
|
} ?: 0
|
||||||
|
} else 0
|
||||||
|
homeAdapter.data = threads
|
||||||
|
if(firstPos >= 0) { manager.scrollToPositionWithOffset(firstPos, offsetTop) }
|
||||||
|
setupMessageRequestsBanner()
|
||||||
|
updateEmptyState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
lifecycleScope.launchWhenStarted {
|
lifecycleScope.launchWhenStarted {
|
||||||
launch(Dispatchers.IO) {
|
launch(Dispatchers.IO) {
|
||||||
// Double check that the long poller is up
|
// Double check that the long poller is up
|
||||||
@ -385,52 +394,20 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
ConfigurationMessageUtilities.syncConfigurationIfNeeded(this@HomeActivity)
|
ConfigurationMessageUtilities.syncConfigurationIfNeeded(this@HomeActivity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the theme hasn't changed then start observing updates again (if it does change then we
|
|
||||||
// will recreate the activity resulting in it responding to changes multiple times)
|
|
||||||
if (currentThemeState == textSecurePreferences.themeState() && !homeViewModel.getObservable(this).hasActiveObservers()) {
|
|
||||||
startObservingUpdates()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPause() {
|
override fun onPause() {
|
||||||
super.onPause()
|
super.onPause()
|
||||||
ApplicationContext.getInstance(this).messageNotifier.setHomeScreenVisible(false)
|
ApplicationContext.getInstance(this).messageNotifier.setHomeScreenVisible(false)
|
||||||
|
|
||||||
homeViewModel.getObservable(this).removeObservers(this)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
val broadcastReceiver = this.broadcastReceiver
|
|
||||||
if (broadcastReceiver != null) {
|
|
||||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(broadcastReceiver)
|
|
||||||
}
|
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
EventBus.getDefault().unregister(this)
|
EventBus.getDefault().unregister(this)
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// region Updating
|
// region Updating
|
||||||
private fun startObservingUpdates() {
|
|
||||||
homeViewModel.getObservable(this).observe(this) { newData ->
|
|
||||||
val manager = binding.recyclerView.layoutManager as LinearLayoutManager
|
|
||||||
val firstPos = manager.findFirstCompletelyVisibleItemPosition()
|
|
||||||
val offsetTop = if(firstPos >= 0) {
|
|
||||||
manager.findViewByPosition(firstPos)?.let { view ->
|
|
||||||
manager.getDecoratedTop(view) - manager.getTopDecorationHeight(view)
|
|
||||||
} ?: 0
|
|
||||||
} else 0
|
|
||||||
homeAdapter.data = newData
|
|
||||||
if(firstPos >= 0) { manager.scrollToPositionWithOffset(firstPos, offsetTop) }
|
|
||||||
setupMessageRequestsBanner()
|
|
||||||
updateEmptyState()
|
|
||||||
}
|
|
||||||
|
|
||||||
ApplicationContext.getInstance(this@HomeActivity).typingStatusRepository.typingThreads.observe(this) { threadIds ->
|
|
||||||
homeAdapter.typingThreadIDs = (threadIds ?: setOf())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateEmptyState() {
|
private fun updateEmptyState() {
|
||||||
val threadCount = (binding.recyclerView.adapter)!!.itemCount
|
val threadCount = (binding.recyclerView.adapter)!!.itemCount
|
||||||
binding.emptyStateContainer.isVisible = threadCount == 0 && binding.recyclerView.isVisible
|
binding.emptyStateContainer.isVisible = threadCount == 0 && binding.recyclerView.isVisible
|
||||||
@ -441,7 +418,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
if (event.recipient.isLocalNumber) {
|
if (event.recipient.isLocalNumber) {
|
||||||
updateProfileButton()
|
updateProfileButton()
|
||||||
} else {
|
} else {
|
||||||
homeViewModel.tryUpdateChannel()
|
homeViewModel.tryReload()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -612,7 +589,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
private fun setConversationPinned(threadId: Long, pinned: Boolean) {
|
private fun setConversationPinned(threadId: Long, pinned: Boolean) {
|
||||||
lifecycleScope.launch(Dispatchers.IO) {
|
lifecycleScope.launch(Dispatchers.IO) {
|
||||||
storage.setPinned(threadId, pinned)
|
storage.setPinned(threadId, pinned)
|
||||||
homeViewModel.tryUpdateChannel()
|
homeViewModel.tryReload()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -688,7 +665,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
|
|||||||
button(R.string.yes) {
|
button(R.string.yes) {
|
||||||
textSecurePreferences.setHasHiddenMessageRequests()
|
textSecurePreferences.setHasHiddenMessageRequests()
|
||||||
setupMessageRequestsBanner()
|
setupMessageRequestsBanner()
|
||||||
homeViewModel.tryUpdateChannel()
|
homeViewModel.tryReload()
|
||||||
}
|
}
|
||||||
button(R.string.no)
|
button(R.string.no)
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,6 @@ import androidx.recyclerview.widget.ListUpdateCallback
|
|||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.RecyclerView.NO_ID
|
import androidx.recyclerview.widget.RecyclerView.NO_ID
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
|
||||||
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests
|
import org.thoughtcrime.securesms.mms.GlideRequests
|
||||||
|
|
||||||
@ -26,14 +25,15 @@ class HomeAdapter(
|
|||||||
|
|
||||||
var header: View? = null
|
var header: View? = null
|
||||||
|
|
||||||
private var _data: List<ThreadRecord> = emptyList()
|
var data: HomeViewModel.Data = HomeViewModel.Data(emptyList(), emptySet())
|
||||||
var data: List<ThreadRecord>
|
|
||||||
get() = _data.toList()
|
|
||||||
set(newData) {
|
set(newData) {
|
||||||
val previousData = _data.toList()
|
if (field === newData) {
|
||||||
val diff = HomeDiffUtil(previousData, newData, context, configFactory)
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val diff = HomeDiffUtil(field, newData, context, configFactory)
|
||||||
val diffResult = DiffUtil.calculateDiff(diff)
|
val diffResult = DiffUtil.calculateDiff(diff)
|
||||||
_data = newData
|
field = newData
|
||||||
diffResult.dispatchUpdatesTo(this as ListUpdateCallback)
|
diffResult.dispatchUpdatesTo(this as ListUpdateCallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,18 +61,10 @@ class HomeAdapter(
|
|||||||
override fun getItemId(position: Int): Long {
|
override fun getItemId(position: Int): Long {
|
||||||
if (hasHeaderView() && position == 0) return NO_ID
|
if (hasHeaderView() && position == 0) return NO_ID
|
||||||
val offsetPosition = if (hasHeaderView()) position-1 else position
|
val offsetPosition = if (hasHeaderView()) position-1 else position
|
||||||
return _data[offsetPosition].threadId
|
return data.threads[offsetPosition].threadId
|
||||||
}
|
}
|
||||||
|
|
||||||
lateinit var glide: GlideRequests
|
lateinit var glide: GlideRequests
|
||||||
var typingThreadIDs = setOf<Long>()
|
|
||||||
set(value) {
|
|
||||||
if (field == value) { return }
|
|
||||||
|
|
||||||
field = value
|
|
||||||
// TODO: replace this with a diffed update or a partial change set with payloads
|
|
||||||
notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder =
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder =
|
||||||
when (viewType) {
|
when (viewType) {
|
||||||
@ -95,8 +87,8 @@ class HomeAdapter(
|
|||||||
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
|
||||||
if (holder is ConversationViewHolder) {
|
if (holder is ConversationViewHolder) {
|
||||||
val offset = if (hasHeaderView()) position - 1 else position
|
val offset = if (hasHeaderView()) position - 1 else position
|
||||||
val thread = data[offset]
|
val thread = data.threads[offset]
|
||||||
val isTyping = typingThreadIDs.contains(thread.threadId)
|
val isTyping = data.typingThreadIDs.contains(thread.threadId)
|
||||||
holder.view.bind(thread, isTyping, glide)
|
holder.view.bind(thread, isTyping, glide)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -113,7 +105,7 @@ class HomeAdapter(
|
|||||||
if (hasHeaderView() && position == 0) HEADER
|
if (hasHeaderView() && position == 0) HEADER
|
||||||
else ITEM
|
else ITEM
|
||||||
|
|
||||||
override fun getItemCount(): Int = data.size + if (hasHeaderView()) 1 else 0
|
override fun getItemCount(): Int = data.threads.size + if (hasHeaderView()) 1 else 0
|
||||||
|
|
||||||
class ConversationViewHolder(val view: ConversationView) : RecyclerView.ViewHolder(view)
|
class ConversationViewHolder(val view: ConversationView) : RecyclerView.ViewHolder(view)
|
||||||
|
|
||||||
|
@ -2,27 +2,26 @@ package org.thoughtcrime.securesms.home
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
|
||||||
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
import org.thoughtcrime.securesms.dependencies.ConfigFactory
|
||||||
import org.thoughtcrime.securesms.util.getConversationUnread
|
import org.thoughtcrime.securesms.util.getConversationUnread
|
||||||
|
|
||||||
class HomeDiffUtil(
|
class HomeDiffUtil(
|
||||||
private val old: List<ThreadRecord>,
|
private val old: HomeViewModel.Data,
|
||||||
private val new: List<ThreadRecord>,
|
private val new: HomeViewModel.Data,
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val configFactory: ConfigFactory
|
private val configFactory: ConfigFactory
|
||||||
): DiffUtil.Callback() {
|
): DiffUtil.Callback() {
|
||||||
|
|
||||||
override fun getOldListSize(): Int = old.size
|
override fun getOldListSize(): Int = old.threads.size
|
||||||
|
|
||||||
override fun getNewListSize(): Int = new.size
|
override fun getNewListSize(): Int = new.threads.size
|
||||||
|
|
||||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
|
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
|
||||||
old[oldItemPosition].threadId == new[newItemPosition].threadId
|
old.threads[oldItemPosition].threadId == new.threads[newItemPosition].threadId
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||||
val oldItem = old[oldItemPosition]
|
val oldItem = old.threads[oldItemPosition]
|
||||||
val newItem = new[newItemPosition]
|
val newItem = new.threads[newItemPosition]
|
||||||
|
|
||||||
// return early to save getDisplayBody or expensive calls
|
// return early to save getDisplayBody or expensive calls
|
||||||
var isSameItem = true
|
var isSameItem = true
|
||||||
@ -47,7 +46,8 @@ class HomeDiffUtil(
|
|||||||
oldItem.isSent == newItem.isSent &&
|
oldItem.isSent == newItem.isSent &&
|
||||||
oldItem.isPending == newItem.isPending &&
|
oldItem.isPending == newItem.isPending &&
|
||||||
oldItem.lastSeen == newItem.lastSeen &&
|
oldItem.lastSeen == newItem.lastSeen &&
|
||||||
configFactory.convoVolatile?.getConversationUnread(newItem) != true
|
configFactory.convoVolatile?.getConversationUnread(newItem) != true &&
|
||||||
|
old.typingThreadIDs.contains(oldItem.threadId) == new.typingThreadIDs.contains(newItem.threadId)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,71 +1,88 @@
|
|||||||
package org.thoughtcrime.securesms.home
|
package org.thoughtcrime.securesms.home
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.asFlow
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import app.cash.copper.flow.observeQuery
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.debounce
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.mapLatest
|
||||||
|
import kotlinx.coroutines.flow.merge
|
||||||
|
import kotlinx.coroutines.flow.onStart
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.thoughtcrime.securesms.ApplicationContext
|
||||||
import org.thoughtcrime.securesms.database.DatabaseContentProviders
|
import org.thoughtcrime.securesms.database.DatabaseContentProviders
|
||||||
import org.thoughtcrime.securesms.database.ThreadDatabase
|
import org.thoughtcrime.securesms.database.ThreadDatabase
|
||||||
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
import org.thoughtcrime.securesms.database.model.ThreadRecord
|
||||||
import java.lang.ref.WeakReference
|
import org.thoughtcrime.securesms.util.observeChanges
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext as ApplicationContextQualifier
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class HomeViewModel @Inject constructor(private val threadDb: ThreadDatabase): ViewModel() {
|
class HomeViewModel @Inject constructor(
|
||||||
|
private val threadDb: ThreadDatabase,
|
||||||
|
private val contentResolver: ContentResolver,
|
||||||
|
@ApplicationContextQualifier private val context: Context,
|
||||||
|
) : ViewModel() {
|
||||||
|
// SharedFlow that emits whenever the user asks us to reload the conversation
|
||||||
|
private val manualReloadTrigger = MutableSharedFlow<Unit>(
|
||||||
|
extraBufferCapacity = 1,
|
||||||
|
onBufferOverflow = BufferOverflow.DROP_OLDEST
|
||||||
|
)
|
||||||
|
|
||||||
private val executor = viewModelScope + SupervisorJob()
|
/**
|
||||||
private var lastContext: WeakReference<Context>? = null
|
* A [StateFlow] that emits the list of threads and the typing status of each thread.
|
||||||
private var updateJobs: MutableList<Job> = mutableListOf()
|
*
|
||||||
|
* This flow will emit whenever the user asks us to reload the conversation list or
|
||||||
|
* whenever the conversation list changes.
|
||||||
|
*/
|
||||||
|
val threads: StateFlow<Data?> = combine(observeConversationList(), observeTypingStatus(), ::Data)
|
||||||
|
.stateIn(viewModelScope, SharingStarted.Eagerly, null)
|
||||||
|
|
||||||
private val _conversations = MutableLiveData<List<ThreadRecord>>()
|
private fun observeTypingStatus(): Flow<Set<Long>> =
|
||||||
val conversations: LiveData<List<ThreadRecord>> = _conversations
|
ApplicationContext.getInstance(context).typingStatusRepository
|
||||||
|
.typingThreads
|
||||||
|
.asFlow()
|
||||||
|
.onStart { emit(emptySet()) }
|
||||||
|
.distinctUntilChanged()
|
||||||
|
|
||||||
private val listUpdateChannel = Channel<Unit>(capacity = Channel.CONFLATED)
|
@Suppress("OPT_IN_USAGE")
|
||||||
|
private fun observeConversationList(): Flow<List<ThreadRecord>> = merge(
|
||||||
fun tryUpdateChannel() = listUpdateChannel.trySend(Unit)
|
manualReloadTrigger,
|
||||||
|
contentResolver.observeChanges(DatabaseContentProviders.ConversationList.CONTENT_URI))
|
||||||
fun getObservable(context: Context): LiveData<List<ThreadRecord>> {
|
.debounce(CHANGE_NOTIFICATION_DEBOUNCE_MILLS)
|
||||||
// If the context has changed (eg. the activity gets recreated) then
|
.onStart { emit(Unit) }
|
||||||
// we need to cancel the old executors and recreate them to prevent
|
.mapLatest { _ ->
|
||||||
// the app from triggering extra updates when data changes
|
withContext(Dispatchers.IO) {
|
||||||
if (context != lastContext?.get()) {
|
threadDb.approvedConversationList.use { openCursor ->
|
||||||
lastContext = WeakReference(context)
|
val reader = threadDb.readerFor(openCursor)
|
||||||
updateJobs.forEach { it.cancel() }
|
buildList(reader.count) {
|
||||||
updateJobs.clear()
|
|
||||||
|
|
||||||
updateJobs.add(
|
|
||||||
executor.launch(Dispatchers.IO) {
|
|
||||||
context.contentResolver
|
|
||||||
.observeQuery(DatabaseContentProviders.ConversationList.CONTENT_URI)
|
|
||||||
.onEach { listUpdateChannel.trySend(Unit) }
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
updateJobs.add(
|
|
||||||
executor.launch(Dispatchers.IO) {
|
|
||||||
for (update in listUpdateChannel) {
|
|
||||||
threadDb.approvedConversationList.use { openCursor ->
|
|
||||||
val reader = threadDb.readerFor(openCursor)
|
|
||||||
val threads = mutableListOf<ThreadRecord>()
|
|
||||||
while (true) {
|
while (true) {
|
||||||
threads += reader.next ?: break
|
add(reader.next ?: break)
|
||||||
}
|
|
||||||
withContext(Dispatchers.Main) {
|
|
||||||
_conversations.value = threads
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
}
|
|
||||||
return conversations
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
fun tryReload() = manualReloadTrigger.tryEmit(Unit)
|
||||||
|
|
||||||
|
data class Data(
|
||||||
|
val threads: List<ThreadRecord>,
|
||||||
|
val typingThreadIDs: Set<Long>
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val CHANGE_NOTIFICATION_DEBOUNCE_MILLS = 100L
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -6,7 +6,6 @@ import android.content.Intent
|
|||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
@ -17,11 +16,17 @@ import android.widget.TextView
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.ColorRes
|
import androidx.annotation.ColorRes
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import network.loki.messenger.R
|
import network.loki.messenger.R
|
||||||
import network.loki.messenger.databinding.ActivityPathBinding
|
import network.loki.messenger.databinding.ActivityPathBinding
|
||||||
import org.session.libsession.snode.OnionRequestAPI
|
import org.session.libsession.snode.OnionRequestAPI
|
||||||
import org.session.libsession.utilities.getColorFromAttr
|
import org.session.libsession.utilities.getColorFromAttr
|
||||||
import org.session.libsignal.utilities.Log
|
|
||||||
import org.session.libsignal.utilities.Snode
|
import org.session.libsignal.utilities.Snode
|
||||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity
|
||||||
import org.thoughtcrime.securesms.util.GlowViewUtilities
|
import org.thoughtcrime.securesms.util.GlowViewUtilities
|
||||||
@ -184,6 +189,7 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
private lateinit var location: Location
|
private lateinit var location: Location
|
||||||
private var dotAnimationStartDelay: Long = 0
|
private var dotAnimationStartDelay: Long = 0
|
||||||
private var dotAnimationRepeatInterval: Long = 0
|
private var dotAnimationRepeatInterval: Long = 0
|
||||||
|
private var job: Job? = null
|
||||||
|
|
||||||
private val dotView by lazy {
|
private val dotView by lazy {
|
||||||
val result = PathDotView(context)
|
val result = PathDotView(context)
|
||||||
@ -240,19 +246,38 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
dotViewLayoutParams.addRule(CENTER_IN_PARENT)
|
dotViewLayoutParams.addRule(CENTER_IN_PARENT)
|
||||||
dotView.layoutParams = dotViewLayoutParams
|
dotView.layoutParams = dotViewLayoutParams
|
||||||
addView(dotView)
|
addView(dotView)
|
||||||
Handler().postDelayed({
|
|
||||||
performAnimation()
|
|
||||||
}, dotAnimationStartDelay)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun performAnimation() {
|
override fun onAttachedToWindow() {
|
||||||
expand()
|
super.onAttachedToWindow()
|
||||||
Handler().postDelayed({
|
|
||||||
collapse()
|
startAnimation()
|
||||||
Handler().postDelayed({
|
}
|
||||||
performAnimation()
|
|
||||||
}, dotAnimationRepeatInterval)
|
override fun onDetachedFromWindow() {
|
||||||
}, 1000)
|
super.onDetachedFromWindow()
|
||||||
|
|
||||||
|
stopAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startAnimation() {
|
||||||
|
job?.cancel()
|
||||||
|
job = GlobalScope.launch {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
while (isActive) {
|
||||||
|
delay(dotAnimationStartDelay)
|
||||||
|
expand()
|
||||||
|
delay(EXPAND_ANIM_DELAY_MILLS)
|
||||||
|
collapse()
|
||||||
|
delay(dotAnimationRepeatInterval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopAnimation() {
|
||||||
|
job?.cancel()
|
||||||
|
job = null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun expand() {
|
private fun expand() {
|
||||||
@ -270,6 +295,10 @@ class PathActivity : PassphraseRequiredActionBarActivity() {
|
|||||||
val endColor = context.resources.getColorWithID(endColorID, context.theme)
|
val endColor = context.resources.getColorWithID(endColorID, context.theme)
|
||||||
GlowViewUtilities.animateShadowColorChange(dotView, startColor, endColor)
|
GlowViewUtilities.animateShadowColorChange(dotView, startColor, endColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val EXPAND_ANIM_DELAY_MILLS = 1000L
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// endregion
|
// endregion
|
||||||
}
|
}
|
@ -73,7 +73,7 @@ public class SignalGlideModule extends AppGlideModule {
|
|||||||
registry.append(DecryptableUri.class, InputStream.class, new DecryptableStreamUriLoader.Factory(context));
|
registry.append(DecryptableUri.class, InputStream.class, new DecryptableStreamUriLoader.Factory(context));
|
||||||
registry.append(AttachmentModel.class, InputStream.class, new AttachmentStreamUriLoader.Factory());
|
registry.append(AttachmentModel.class, InputStream.class, new AttachmentStreamUriLoader.Factory());
|
||||||
registry.append(ChunkedImageUrl.class, InputStream.class, new ChunkedImageUrlLoader.Factory());
|
registry.append(ChunkedImageUrl.class, InputStream.class, new ChunkedImageUrlLoader.Factory());
|
||||||
registry.append(PlaceholderAvatarPhoto.class, BitmapDrawable.class, new PlaceholderAvatarLoader.Factory());
|
registry.append(PlaceholderAvatarPhoto.class, BitmapDrawable.class, new PlaceholderAvatarLoader.Factory(context));
|
||||||
registry.replace(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory());
|
registry.replace(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
package org.thoughtcrime.securesms.util
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.database.ContentObserver
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.Looper
|
||||||
|
import androidx.annotation.CheckResult
|
||||||
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Observe changes to a content Uri. This function will emit the Uri whenever the content or
|
||||||
|
* its descendants change, according to the parameter [notifyForDescendants].
|
||||||
|
*/
|
||||||
|
@CheckResult
|
||||||
|
fun ContentResolver.observeChanges(uri: Uri, notifyForDescendants: Boolean = false): Flow<Uri> {
|
||||||
|
return callbackFlow {
|
||||||
|
val observer = object : ContentObserver(Handler(Looper.getMainLooper())) {
|
||||||
|
override fun onChange(selfChange: Boolean) {
|
||||||
|
trySend(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registerContentObserver(uri, notifyForDescendants, observer)
|
||||||
|
awaitClose {
|
||||||
|
unregisterContentObserver(observer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -55,7 +55,7 @@ class IP2Country private constructor(private val context: Context) {
|
|||||||
|
|
||||||
public fun configureIfNeeded(context: Context) {
|
public fun configureIfNeeded(context: Context) {
|
||||||
if (isInitialized) { return; }
|
if (isInitialized) { return; }
|
||||||
shared = IP2Country(context)
|
shared = IP2Country(context.applicationContext)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,13 +1,10 @@
|
|||||||
package org.session.libsession.avatars
|
package org.session.libsession.avatars
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import com.bumptech.glide.load.Key
|
import com.bumptech.glide.load.Key
|
||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
|
|
||||||
class PlaceholderAvatarPhoto(val context: Context,
|
class PlaceholderAvatarPhoto(val hashString: String,
|
||||||
val hashString: String,
|
|
||||||
val displayName: String): Key {
|
val displayName: String): Key {
|
||||||
|
|
||||||
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
|
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
|
||||||
messageDigest.update(hashString.encodeToByteArray())
|
messageDigest.update(hashString.encodeToByteArray())
|
||||||
messageDigest.update(displayName.encodeToByteArray())
|
messageDigest.update(displayName.encodeToByteArray())
|
||||||
|
@ -70,7 +70,7 @@ public class Recipient implements RecipientModifiedListener {
|
|||||||
private final @NonNull Address address;
|
private final @NonNull Address address;
|
||||||
private final @NonNull List<Recipient> participants = new LinkedList<>();
|
private final @NonNull List<Recipient> participants = new LinkedList<>();
|
||||||
|
|
||||||
private Context context;
|
private final Context context;
|
||||||
private @Nullable String name;
|
private @Nullable String name;
|
||||||
private @Nullable String customLabel;
|
private @Nullable String customLabel;
|
||||||
private boolean resolving;
|
private boolean resolving;
|
||||||
@ -132,7 +132,7 @@ public class Recipient implements RecipientModifiedListener {
|
|||||||
@NonNull Optional<RecipientDetails> details,
|
@NonNull Optional<RecipientDetails> details,
|
||||||
@NonNull ListenableFutureTask<RecipientDetails> future)
|
@NonNull ListenableFutureTask<RecipientDetails> future)
|
||||||
{
|
{
|
||||||
this.context = context;
|
this.context = context.getApplicationContext();
|
||||||
this.address = address;
|
this.address = address;
|
||||||
this.color = null;
|
this.color = null;
|
||||||
this.resolving = true;
|
this.resolving = true;
|
||||||
@ -259,7 +259,7 @@ public class Recipient implements RecipientModifiedListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Recipient(@NonNull Context context, @NonNull Address address, @NonNull RecipientDetails details) {
|
Recipient(@NonNull Context context, @NonNull Address address, @NonNull RecipientDetails details) {
|
||||||
this.context = context;
|
this.context = context.getApplicationContext();
|
||||||
this.address = address;
|
this.address = address;
|
||||||
this.contactUri = details.contactUri;
|
this.contactUri = details.contactUri;
|
||||||
this.name = details.name;
|
this.name = details.name;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user