diff --git a/jami-android/app/src/main/java/cx/ring/client/AccountAdapter.kt b/jami-android/app/src/main/java/cx/ring/client/AccountAdapter.kt index a47bfd0d878a11b91764dc2037dc0cef7faab67f..0154f2d3b5ceab39d0fa8519dc46f9d4f47dde6e 100644 --- a/jami-android/app/src/main/java/cx/ring/client/AccountAdapter.kt +++ b/jami-android/app/src/main/java/cx/ring/client/AccountAdapter.kt @@ -107,7 +107,7 @@ class AccountAdapter( profile.first, profile.second, true, - profile.first.isRegistered + profile.first.presenceStatus ) ) holder.binding.title.text = getTitle(profile.first, profile.second) diff --git a/jami-android/app/src/main/java/cx/ring/fragments/HomeFragment.kt b/jami-android/app/src/main/java/cx/ring/fragments/HomeFragment.kt index bb4d56c541fca450b0d3f6b0f092e9f4568bfc0c..450d35a1da347adecf69166a870cac59d15d9e89 100644 --- a/jami-android/app/src/main/java/cx/ring/fragments/HomeFragment.kt +++ b/jami-android/app/src/main/java/cx/ring/fragments/HomeFragment.kt @@ -524,7 +524,7 @@ class HomeFragment: BaseSupportFragment<HomePresenter, HomeView>(), profile.first, profile.second, true, - profile.first.isRegistered + profile.first.presenceStatus ), TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, diff --git a/jami-android/app/src/main/java/cx/ring/tv/cards/contacts/ContactCardPresenter.kt b/jami-android/app/src/main/java/cx/ring/tv/cards/contacts/ContactCardPresenter.kt index d14e083454b85a0861bc755f9a2bae7d17149847..5adfcc256e7d3d24357b533d67a56474ef7cc719 100644 --- a/jami-android/app/src/main/java/cx/ring/tv/cards/contacts/ContactCardPresenter.kt +++ b/jami-android/app/src/main/java/cx/ring/tv/cards/contacts/ContactCardPresenter.kt @@ -27,6 +27,7 @@ import cx.ring.tv.cards.CardView import cx.ring.views.AvatarDrawable import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.disposables.CompositeDisposable +import net.jami.model.Contact import net.jami.services.ConversationFacade class ContactCardPresenter(context: Context, val conversationFacade: ConversationFacade, resId: Int) : @@ -42,7 +43,7 @@ class ContactCardPresenter(context: Context, val conversationFacade: Conversatio val title: String, val uri: String, val avatar: Drawable, - val isOnline: Boolean + val presenceStatus: Contact.PresenceStatus ) override fun onBindViewHolder(card: Card, cardView: CardView, disposable: CompositeDisposable) { @@ -52,12 +53,12 @@ class ContactCardPresenter(context: Context, val conversationFacade: Conversatio .withViewModel(vm) .withPresence(false) .withCircleCrop(false) - .build(context), vm.isOnline) } + .build(context), vm.presenceStatus) } .observeOn(AndroidSchedulers.mainThread()) .subscribe { vm -> cardView.apply { titleText = vm.title contentText = vm.uri - badgeImage = if (vm.isOnline) badge else null + badgeImage = if (vm.presenceStatus != Contact.PresenceStatus.OFFLINE) badge else null setMainImage(vm.avatar, true) } }) diff --git a/jami-android/app/src/main/java/cx/ring/views/AvatarDrawable.kt b/jami-android/app/src/main/java/cx/ring/views/AvatarDrawable.kt index bb997eeeabdf0f1ab7e314abe502f455f8bae010..86c50a301d77a6eed4ff21fe8ca359024f9f3976 100644 --- a/jami-android/app/src/main/java/cx/ring/views/AvatarDrawable.kt +++ b/jami-android/app/src/main/java/cx/ring/views/AvatarDrawable.kt @@ -21,6 +21,7 @@ import android.graphics.* import android.graphics.drawable.Drawable import android.graphics.drawable.VectorDrawable import android.util.TypedValue +import androidx.annotation.ColorInt import androidx.annotation.ColorRes import androidx.core.content.ContextCompat import cx.ring.R @@ -65,9 +66,13 @@ class AvatarDrawable : Drawable { } private val presenceFillPaint: Paint private val presenceStrokePaint: Paint + @ColorInt + private val presenceConnectedColor: Int + @ColorInt + private val presenceAvailableColor: Int private val checkedPaint: Paint private val cropCircle: Boolean - private var isOnline = false + private var presenceStatus = Contact.PresenceStatus.OFFLINE private var isChecked = false private var showPresence = false @@ -76,10 +81,6 @@ class AvatarDrawable : Drawable { private const val SIZE_AB = 36 private const val SIZE_BORDER = 2 private const val DEFAULT_TEXT_SIZE_PERCENTAGE = 0.5f - private const val PLACEHOLDER_ICON = R.drawable.baseline_account_crop_24 - private const val PLACEHOLDER_ICON_GROUP = R.drawable.baseline_group_24 - private const val CHECKED_ICON = R.drawable.baseline_check_circle_24 - private const val PRESENCE_COLOR = R.color.online_indicator private val contactColors = intArrayOf( R.color.red_500, R.color.pink_500, R.color.purple_500, R.color.deep_purple_500, @@ -99,13 +100,13 @@ class AvatarDrawable : Drawable { VCardServiceImpl.loadProfile(context, account) .map { profile -> build(context, account, profile, crop) } - fun build(context: Context, account: Account, profile: Profile, crop: Boolean = true, isOnline: Boolean = false) = + fun build(context: Context, account: Account, profile: Profile, crop: Boolean = true, presenceStatus: Contact.PresenceStatus = Contact.PresenceStatus.OFFLINE) = Builder() .withPhoto(profile.avatar as Bitmap?) .withNameData(profile.displayName, account.registeredName) .withId(if (account.isSip) account.uri else Uri(Uri.JAMI_URI_SCHEME, account.username!!).rawUriString) .withCircleCrop(crop) - .withOnlineState(isOnline) + .withOnlineState(presenceStatus) .build(context) private fun getSubBounds(bounds: Rect, total: Int, i: Int): Rect? { @@ -171,7 +172,7 @@ class AvatarDrawable : Drawable { private var name: String? = null private var id: String? = null private var circleCrop = false - private var isOnline = false + private var presenceStatus = Contact.PresenceStatus.OFFLINE private var showPresence = true private var isChecked = false private var isGroup = false @@ -200,8 +201,8 @@ class AvatarDrawable : Drawable { return this } - fun withOnlineState(isOnline: Boolean): Builder { - this.isOnline = isOnline + fun withOnlineState(status: Contact.PresenceStatus): Builder { + this.presenceStatus = status return this } @@ -221,7 +222,7 @@ class AvatarDrawable : Drawable { fun withContact(contact: ContactViewModel?) = if (contact == null) this else withPhoto(contact.profile.avatar as? Bitmap?) .withId(contact.contact.uri.toString()) - .withPresence(contact.presence) + .withPresence(contact.presence != Contact.PresenceStatus.OFFLINE) .withOnlineState(contact.presence) .withNameData(contact.profile.displayName, contact.registeredName) @@ -278,12 +279,12 @@ class AvatarDrawable : Drawable { .setGroup() else withContact(ConversationItemViewModel.getContact(vm.contacts)) .withPresence(vm.showPresence) - .withOnlineState(vm.isOnline) + .withOnlineState(vm.presenceStatus) .withCheck(vm.isChecked) fun build(context: Context): AvatarDrawable = AvatarDrawable(context, photos, name, id, circleCrop, isGroup).also { - it.setOnline(isOnline) + it.setPresenceStatus(presenceStatus) it.setChecked(isChecked) it.showPresence = showPresence } @@ -296,7 +297,7 @@ class AvatarDrawable : Drawable { contact.profile.avatar?.let { photo -> bitmaps?.set(0, photo as Bitmap) } - isOnline = contact.presence + presenceStatus = contact.presence update = true } @@ -310,8 +311,9 @@ class AvatarDrawable : Drawable { update = true } - fun setOnline(online: Boolean) { - isOnline = online + fun setPresenceStatus(status: Contact.PresenceStatus) { + presenceStatus = status + presenceFillPaint.color = if (status == Contact.PresenceStatus.CONNECTED) presenceConnectedColor else presenceAvailableColor } fun setChecked(checked: Boolean) { @@ -363,17 +365,18 @@ class AvatarDrawable : Drawable { clipPaint = if (cropCircle) arrayOf(Paint()) else null if (avatarText == null) { placeholder = - context.getDrawable(if (isGroup) PLACEHOLDER_ICON_GROUP else PLACEHOLDER_ICON) as VectorDrawable? + context.getDrawable(if (isGroup) R.drawable.baseline_group_24 else R.drawable.baseline_account_crop_24) as VectorDrawable? } else { textPaint.color = Color.WHITE textPaint.typeface = Typeface.SANS_SERIF } } + presenceAvailableColor = ContextCompat.getColor(context, R.color.available_indicator) + presenceConnectedColor = ContextCompat.getColor(context, R.color.online_indicator) presenceFillPaint = Paint().apply { - color = ContextCompat.getColor(context, PRESENCE_COLOR) style = Paint.Style.FILL isAntiAlias = true - color = ContextCompat.getColor(context, PRESENCE_COLOR) + color = presenceConnectedColor } val typedValue = TypedValue() @@ -388,7 +391,7 @@ class AvatarDrawable : Drawable { style = Paint.Style.STROKE isAntiAlias = true } - checkedIcon = context.getDrawable(CHECKED_ICON) as VectorDrawable? + checkedIcon = context.getDrawable(R.drawable.baseline_check_circle_24) as VectorDrawable? checkedIcon?.setTint(ContextCompat.getColor(context, R.color.colorPrimary)) checkedPaint = Paint().apply { color = ContextCompat.getColor(context, R.color.background) @@ -415,9 +418,11 @@ class AvatarDrawable : Drawable { clipPaint = if (other.clipPaint == null) null else Array(other.clipPaint.size) { i -> Paint(other.clipPaint[i]).apply { shader = null } } - isOnline = other.isOnline + presenceStatus = other.presenceStatus isChecked = other.isChecked showPresence = other.showPresence + presenceConnectedColor = other.presenceConnectedColor + presenceAvailableColor = other.presenceAvailableColor presenceFillPaint = other.presenceFillPaint presenceStrokePaint = other.presenceStrokePaint checkedPaint = other.checkedPaint @@ -468,7 +473,7 @@ class AvatarDrawable : Drawable { } else { finalCanvas.drawBitmap(firstWorkspace, null, bounds, drawPaint) } - if (showPresence && isOnline) { + if (showPresence && presenceStatus != Contact.PresenceStatus.OFFLINE) { drawPresence(finalCanvas) } if (isChecked) { diff --git a/jami-android/app/src/main/res/values/colors.xml b/jami-android/app/src/main/res/values/colors.xml index ccca7481413390eaebeedfc85bc5c32bceda683a..24781b33ff4c44ab32f047657a4ab00199e67251 100644 --- a/jami-android/app/src/main/res/values/colors.xml +++ b/jami-android/app/src/main/res/values/colors.xml @@ -42,6 +42,7 @@ <color name="background_status_recommended">#8ee0d0</color> <color name="background_status_required">#fee4e9</color> + <color name="available_indicator">#E59028</color> <color name="online_indicator">#0B8271</color> <color name="menu_icon">#0A84FF</color> diff --git a/jami-android/libjamiclient/src/main/kotlin/net/jami/model/Account.kt b/jami-android/libjamiclient/src/main/kotlin/net/jami/model/Account.kt index b2166205e63d96839a5b8fc8c4fbea98c82ad802..ef10c314a8e032b12864a748bd4821e4a50ddf9d 100644 --- a/jami-android/libjamiclient/src/main/kotlin/net/jami/model/Account.kt +++ b/jami-android/libjamiclient/src/main/kotlin/net/jami/model/Account.kt @@ -494,6 +494,11 @@ class Account( get() = registrationState == AccountConfig.RegistrationState.TRYING val isRegistered: Boolean get() = registrationState == AccountConfig.RegistrationState.REGISTERED + val presenceStatus: Contact.PresenceStatus + get() = if (isRegistered) Contact.PresenceStatus.CONNECTED + else if (isTrying) Contact.PresenceStatus.AVAILABLE + else Contact.PresenceStatus.OFFLINE + val isInError: Boolean get() { val state = registrationState @@ -787,9 +792,13 @@ class Account( } fun presenceUpdate(contactUri: String, status: Int) { - //Log.w(TAG, "presenceUpdate " + contactUri + " " + isOnline); + //Log.w(TAG, "presenceUpdate $contactUri $status"); val contact = getContactFromCache(contactUri) - contact.setPresence(status > 0) + contact.setPresence(when (status) { + 0 -> Contact.PresenceStatus.OFFLINE + 1 -> Contact.PresenceStatus.AVAILABLE + else -> Contact.PresenceStatus.CONNECTED + }) synchronized(conversations) { conversations[contactUri]?.let { conversationRefreshed(it) } } diff --git a/jami-android/libjamiclient/src/main/kotlin/net/jami/model/Contact.kt b/jami-android/libjamiclient/src/main/kotlin/net/jami/model/Contact.kt index cf4ea5901845dca2b6b5b9d87ab2ba0599ae9079..ccb2c72e7fef85c5ebfdd3ecf66cac540abfecf0 100644 --- a/jami-android/libjamiclient/src/main/kotlin/net/jami/model/Contact.kt +++ b/jami-android/libjamiclient/src/main/kotlin/net/jami/model/Contact.kt @@ -29,8 +29,8 @@ class Contact constructor(val uri: Uri, val isUser: Boolean = false) { } var username: Single<String>? = null - var presenceUpdates: Observable<Boolean>? = null - private var mContactPresenceEmitter: Emitter<Boolean>? = null + var presenceUpdates: Observable<PresenceStatus>? = null + private var mContactPresenceEmitter: Emitter<PresenceStatus>? = null private val profileSubject: Subject<Single<Profile>> = BehaviorSubject.create() val profile: Observable<Profile> = profileSubject.switchMapSingle { single -> single } var loadedProfile: Single<Profile>? = null @@ -63,7 +63,7 @@ class Contact constructor(val uri: Uri, val isUser: Boolean = false) { val updatesSubject: Observable<Contact> get() = mContactUpdates - fun setPresenceEmitter(emitter: Emitter<Boolean>?) { + fun setPresenceEmitter(emitter: Emitter<PresenceStatus>?) { mContactPresenceEmitter?.let { e -> if (e != emitter) e.onComplete() @@ -71,7 +71,13 @@ class Contact constructor(val uri: Uri, val isUser: Boolean = false) { mContactPresenceEmitter = emitter } - fun setPresence(present: Boolean) { + enum class PresenceStatus { + OFFLINE, + AVAILABLE, + CONNECTED + } + + fun setPresence(present: PresenceStatus) { mContactPresenceEmitter?.onNext(present) } diff --git a/jami-android/libjamiclient/src/main/kotlin/net/jami/model/Profile.kt b/jami-android/libjamiclient/src/main/kotlin/net/jami/model/Profile.kt index a7a28d7557ad1f0dc7ad8613591c171bcd9b6f4b..392c37228ba49b326c5b561a53a423a89a97d27d 100644 --- a/jami-android/libjamiclient/src/main/kotlin/net/jami/model/Profile.kt +++ b/jami-android/libjamiclient/src/main/kotlin/net/jami/model/Profile.kt @@ -26,7 +26,7 @@ open class Profile(val displayName: String?, val avatar: Any?, val description: } } -class ContactViewModel(val contact: Contact, val profile: Profile, val registeredName: String? = null, val presence: Boolean = false) { +class ContactViewModel(val contact: Contact, val profile: Profile, val registeredName: String? = null, val presence: Contact.PresenceStatus = Contact.PresenceStatus.OFFLINE) { val displayUri: String get() = registeredName ?: contact.uri.toString() val displayName: String diff --git a/jami-android/libjamiclient/src/main/kotlin/net/jami/services/ContactService.kt b/jami-android/libjamiclient/src/main/kotlin/net/jami/services/ContactService.kt index 28ea9514cb4dd9074a81bb7a3cd05874de93d39e..fd70e903b903f35d95b7df46e006beee1292467a 100644 --- a/jami-android/libjamiclient/src/main/kotlin/net/jami/services/ContactService.kt +++ b/jami-android/libjamiclient/src/main/kotlin/net/jami/services/ContactService.kt @@ -75,14 +75,14 @@ abstract class ContactService( val uriString = contact.uri.rawRingId synchronized(contact) { val presenceUpdates = contact.presenceUpdates ?: run { - Observable.create { emitter: ObservableEmitter<Boolean> -> - emitter.onNext(false) + Observable.create { emitter: ObservableEmitter<Contact.PresenceStatus> -> + emitter.onNext(Contact.PresenceStatus.OFFLINE) contact.setPresenceEmitter(emitter) mAccountService.subscribeBuddy(accountId, uriString, true) emitter.setCancellable { mAccountService.subscribeBuddy(accountId, uriString, false) contact.setPresenceEmitter(null) - emitter.onNext(false) + emitter.onNext(Contact.PresenceStatus.OFFLINE) } } .replay(1) @@ -105,7 +105,7 @@ abstract class ContactService( return if (contact.isUser) { mAccountService.getObservableAccountProfile(accountId).map { profile -> - ContactViewModel(contact, profile.second, profile.first.registeredName.ifEmpty { null }, withPresence && profile.first.isRegistered) } + ContactViewModel(contact, profile.second, profile.first.registeredName.ifEmpty { null }, if (withPresence) profile.first.presenceStatus else Contact.PresenceStatus.OFFLINE) } } else { if (contact.loadedProfile == null) { contact.loadedProfile = loadContactData(contact, accountId).cache() @@ -116,7 +116,7 @@ abstract class ContactService( { profile, name, presence -> ContactViewModel(contact, profile, name.ifEmpty { null }, presence) } else Observable.combineLatest(contact.profile, username.toObservable()) - { profile, name -> ContactViewModel(contact, profile, name.ifEmpty { null }, false) } + { profile, name -> ContactViewModel(contact, profile, name.ifEmpty { null }, Contact.PresenceStatus.OFFLINE) } } } } diff --git a/jami-android/libjamiclient/src/main/kotlin/net/jami/smartlist/ConversationItemViewModel.kt b/jami-android/libjamiclient/src/main/kotlin/net/jami/smartlist/ConversationItemViewModel.kt index 1e54060da60727d2b0d0d3b4ca10e8ccafb883c0..04ab85ecf65794ecb9e5f9de7b9cfeac29fa3f4e 100644 --- a/jami-android/libjamiclient/src/main/kotlin/net/jami/smartlist/ConversationItemViewModel.kt +++ b/jami-android/libjamiclient/src/main/kotlin/net/jami/smartlist/ConversationItemViewModel.kt @@ -31,7 +31,18 @@ class ConversationItemViewModel( val mode: Conversation.Mode = conversation.mode.blockingFirst() val uuid: String = uri.rawUriString val title: String = getTitle(conversation, conversationProfile, contacts) - val isOnline: Boolean = showPresence && contacts.firstOrNull { it.presence } != null + val presenceStatus: Contact.PresenceStatus = if (showPresence) + contacts.let { + var status = Contact.PresenceStatus.OFFLINE + for (contact in it) { + if (contact.presence == Contact.PresenceStatus.CONNECTED) + return@let Contact.PresenceStatus.CONNECTED + else if (contact.presence == Contact.PresenceStatus.AVAILABLE) + status = Contact.PresenceStatus.AVAILABLE + } + status + } else Contact.PresenceStatus.OFFLINE + var isChecked = false var selected: Observable<Boolean>? = conversation.getVisible() private set @@ -79,7 +90,7 @@ class ConversationItemViewModel( if (other !is ConversationItemViewModel) return false return contacts === other.contacts && title == other.title - && isOnline == other.isOnline + && presenceStatus == other.presenceStatus } companion object {