Chat Effects

This commit is contained in:
Valere 2020-12-09 12:34:22 +01:00 committed by Benoit Marty
parent a027ef29e5
commit 7da8b13cde
21 changed files with 339 additions and 68 deletions

View File

@ -8,6 +8,7 @@ Features ✨:
- Store encrypted file in cache and cleanup decrypted file at each app start (#2512)
- Emoji Keyboard (#2520)
- Social login (#2452)
- Support for chat effects in timeline (confetti, snow) (#2535)
Improvements 🙌:
- Add Setting Item to Change PIN (#2462)

View File

@ -43,6 +43,10 @@ allprojects {
includeGroupByRegex 'com\\.github\\.chrisbanes'
// PFLockScreen-Android
includeGroupByRegex 'com\\.github\\.vector-im'
//Chat effects
includeGroupByRegex 'com\\.github\\.jetradarmobile'
includeGroupByRegex 'nl\\.dionsegijn'
}
}
maven {

View File

@ -33,4 +33,7 @@ object MessageType {
// Add, in local, a fake message type in order to StickerMessage can inherit Message class
// Because sticker isn't a message type but a event type without msgtype field
const val MSGTYPE_STICKER_LOCAL = "org.matrix.android.sdk.sticker"
const val MSGTYPE_CONFETTI = "nic.custom.confetti"
const val MSGTYPE_SNOW = "nic.custom.snow"
}

View File

@ -410,6 +410,9 @@ dependencies {
// Badge for compatibility
implementation 'me.leolin:ShortcutBadger:1.1.22@aar'
// Chat effects
implementation 'nl.dionsegijn:konfetti:1.2.5'
implementation 'com.github.jetradarmobile:android-snowfall:1.2.0'
// DI
implementation "com.google.dagger:dagger:$daggerVersion"
kapt "com.google.dagger:dagger-compiler:$daggerVersion"

View File

@ -390,6 +390,11 @@ SOFTWARE.
<br/>
Copyright (C) 2016 - Niklas Baudy, Ruben Gees, Mario Đanić and contributors
</li>
<li>
<b>JetradarMobile / android-snowfall</b>
<br/>
Copyright 2016 JetRadar
</li>
</ul>
<pre>
Apache License
@ -576,5 +581,14 @@ Apache License
</li>
</pre>
<pre>
ISC License
<li>
<b>DanielMartinus / Konfetti</b>
<br/>
Copyright (c) 2017 Dion Segijn
</li>
</pre>
</body>
</html>

View File

@ -44,7 +44,8 @@ enum class Command(val command: String, val parameters: String, @StringRes val d
POLL("/poll", "Question | Option 1 | Option 2 ...", R.string.command_description_poll),
SHRUG("/shrug", "<message>", R.string.command_description_shrug),
PLAIN("/plain", "<message>", R.string.command_description_plain),
DISCARD_SESSION("/discardsession", "", R.string.command_description_discard_session);
DISCARD_SESSION("/discardsession", "", R.string.command_description_discard_session),
CONFETTI("/confetti", "<message>", R.string.command_confetti);
val length
get() = command.length + 1

View File

@ -291,6 +291,10 @@ object CommandParser {
Command.DISCARD_SESSION.command -> {
ParsedCommand.DiscardSession
}
Command.CONFETTI.command -> {
val message = textMessage.substring(Command.CONFETTI.command.length).trim()
ParsedCommand.Confetti(message)
}
else -> {
// Unknown command
ParsedCommand.ErrorUnknownSlashCommand(slashCommand)

View File

@ -55,4 +55,5 @@ sealed class ParsedCommand {
class SendShrug(val message: CharSequence) : ParsedCommand()
class SendPoll(val question: String, val options: List<String>) : ParsedCommand()
object DiscardSession : ParsedCommand()
class Confetti(val message: String) : ParsedCommand()
}

View File

@ -0,0 +1,125 @@
/*
* Copyright (c) 2020 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.app.features.home.room.detail
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.message.MessageContent
import org.matrix.android.sdk.api.session.room.model.message.MessageType
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import java.util.Timer
import java.util.TimerTask
import javax.inject.Inject
enum class ChatEffect {
CONFETTI,
SNOW
}
/**
* A simple chat effect manager helper class
* Used by the view model to know if an event that become visible should trigger a chat effect.
* It also manages effect duration and some cool down, for example if an effect is currently playing,
* any other trigger will be ignored
* For now it uses visibility callback to check for an effect (that means that a fail to decrypt event - more
* precisly an event decrypted with a few delay won't trigger an effect; it's acceptable)
* Events that are more that 10s old won't trigger effects
*/
class ChatEffectManager @Inject constructor() {
interface Delegate {
fun stopEffects()
fun shouldStartEffect(effect: ChatEffect)
}
var delegate: Delegate? = null
private var stopTimer: Timer? = null
// an in memory store to avoid trigger twice for an event (quick close/open timeline)
private val alreadyPlayed = emptyList<String>().toMutableList()
fun checkForEffect(event: TimelineEvent) {
val age = event.root.ageLocalTs ?: 0
val now = System.currentTimeMillis()
// messages older than 10s should not trigger any effect
if ((now - age) >= 10_000) return
val content = event.root.getClearContent()?.toModel<MessageContent>() ?: return
val effect = findEffect(content, event)
if (effect != null) {
synchronized(this) {
if (hasAlreadyPlayed(event)) return
markAsAlreadyPlayed(event)
// there is already an effect playing, so ignore
if (stopTimer != null) return
delegate?.shouldStartEffect(effect)
stopTimer = Timer().apply {
schedule(object : TimerTask() {
override fun run() {
stopEffect()
}
}, 6_000)
}
}
}
}
fun dispose() {
stopTimer?.cancel()
stopTimer = null
delegate = null
alreadyPlayed.clear()
}
@Synchronized
private fun stopEffect() {
stopTimer = null
delegate?.stopEffects()
}
private fun markAsAlreadyPlayed(event: TimelineEvent) {
alreadyPlayed.add(event.eventId)
// also put the tx id as fast way to deal with local echo
event.root.unsignedData?.transactionId?.let {
alreadyPlayed.add(it)
}
}
private fun hasAlreadyPlayed(event: TimelineEvent) : Boolean {
return alreadyPlayed.contains(event.eventId)
|| (event.root.unsignedData?.transactionId?.let { alreadyPlayed.contains(it) } ?: false)
}
private fun findEffect(content: MessageContent, event: TimelineEvent): ChatEffect? {
return when (content.msgType) {
MessageType.MSGTYPE_CONFETTI -> ChatEffect.CONFETTI
MessageType.MSGTYPE_SNOW -> ChatEffect.SNOW
MessageType.MSGTYPE_TEXT -> {
val text = event.root.getClearContent().toModel<MessageContent>()?.body ?: ""
if (text.contains("🎉")
|| text.contains("🎊")) {
ChatEffect.CONFETTI
} else if (text.contains("⛄️")
|| text.contains("☃️")
|| text.contains("❄️")) {
ChatEffect.SNOW
} else null
}
else -> null
}
}
}

View File

@ -21,6 +21,7 @@ import android.app.Activity
import android.content.DialogInterface
import android.content.Intent
import android.content.res.Configuration
import android.graphics.Color
import android.graphics.Typeface
import android.net.Uri
import android.os.Build
@ -48,11 +49,14 @@ import androidx.core.text.toSpannable
import androidx.core.util.Pair
import androidx.core.view.ViewCompat
import androidx.core.view.forEach
import androidx.core.view.isInvisible
import androidx.core.view.isVisible
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.transition.TransitionManager
import butterknife.BindView
import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.OnModelBuildFinishedListener
import com.airbnb.epoxy.addGlidePreloader
@ -168,6 +172,8 @@ import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_room_detail.*
import kotlinx.android.synthetic.main.composer_layout.view.*
import kotlinx.android.synthetic.main.merge_overlay_waiting_view.*
import nl.dionsegijn.konfetti.models.Shape
import nl.dionsegijn.konfetti.models.Size
import org.billcarsonfr.jsonviewer.JSonViewerDialog
import org.commonmark.parser.Parser
import org.matrix.android.sdk.api.MatrixCallback
@ -378,6 +384,8 @@ class RoomDetailFragment @Inject constructor(
is RoomDetailViewEvents.ShowRoomAvatarFullScreen -> it.matrixItem?.let { item ->
navigator.openBigImageViewer(requireActivity(), it.view, item)
}
is RoomDetailViewEvents.StartChatEffect -> handleChatEffect(it.type)
RoomDetailViewEvents.StopChatEffects -> handleStopChatEffects()
}.exhaustive
}
@ -386,6 +394,34 @@ class RoomDetailFragment @Inject constructor(
}
}
private fun handleChatEffect(chatEffect: ChatEffect) {
when (chatEffect) {
ChatEffect.CONFETTI -> {
viewKonfetti.isVisible = true
viewKonfetti.build()
.addColors(Color.YELLOW, Color.GREEN, Color.MAGENTA)
.setDirection(0.0, 359.0)
.setSpeed(2f, 5f)
.setFadeOutEnabled(true)
.setTimeToLive(2000L)
.addShapes(Shape.Square, Shape.Circle)
.addSizes(Size(12))
.setPosition(-50f, viewKonfetti.width + 50f, -50f, -50f)
.streamFor(150, 3000L)
}
ChatEffect.SNOW -> {
viewSnowFall.isVisible = true
viewSnowFall.restartFalling()
}
}
}
private fun handleStopChatEffects() {
TransitionManager.beginDelayedTransition(rootConstraintLayout)
viewSnowFall.isVisible = false
// when gone the effect is a bit buggy
viewKonfetti.isInvisible = true
}
override fun onImageReady(uri: Uri?) {
uri ?: return
roomDetailViewModel.handle(

View File

@ -95,4 +95,7 @@ sealed class RoomDetailViewEvents : VectorViewEvents {
// TODO Remove
object SlashCommandNotImplemented : SendMessageResult()
data class StartChatEffect(val type: ChatEffect) : RoomDetailViewEvents()
object StopChatEffects : RoomDetailViewEvents()
}

View File

@ -98,7 +98,6 @@ import org.matrix.android.sdk.rx.rx
import org.matrix.android.sdk.rx.unwrap
import timber.log.Timber
import java.io.File
import java.lang.Exception
import java.util.UUID
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
@ -115,8 +114,9 @@ class RoomDetailViewModel @AssistedInject constructor(
private val roomSummaryHolder: RoomSummaryHolder,
private val typingHelper: TypingHelper,
private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager,
private val chatEffectManager: ChatEffectManager,
timelineSettingsFactory: TimelineSettingsFactory
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState), Timeline.Listener {
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState), Timeline.Listener, ChatEffectManager.Delegate {
private val room = session.getRoom(initialState.roomId)!!
private val eventId = initialState.eventId
@ -171,6 +171,7 @@ class RoomDetailViewModel @AssistedInject constructor(
room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear()
// Inform the SDK that the room is displayed
session.onRoomDisplayed(initialState.roomId)
chatEffectManager.delegate = this
}
private fun observePowerLevel() {
@ -225,58 +226,58 @@ class RoomDetailViewModel @AssistedInject constructor(
override fun handle(action: RoomDetailAction) {
when (action) {
is RoomDetailAction.UserIsTyping -> handleUserIsTyping(action)
is RoomDetailAction.SaveDraft -> handleSaveDraft(action)
is RoomDetailAction.SendMessage -> handleSendMessage(action)
is RoomDetailAction.SendMedia -> handleSendMedia(action)
is RoomDetailAction.SendSticker -> handleSendSticker(action)
is RoomDetailAction.TimelineEventTurnsVisible -> handleEventVisible(action)
is RoomDetailAction.TimelineEventTurnsInvisible -> handleEventInvisible(action)
is RoomDetailAction.LoadMoreTimelineEvents -> handleLoadMore(action)
is RoomDetailAction.SendReaction -> handleSendReaction(action)
is RoomDetailAction.AcceptInvite -> handleAcceptInvite()
is RoomDetailAction.RejectInvite -> handleRejectInvite()
is RoomDetailAction.RedactAction -> handleRedactEvent(action)
is RoomDetailAction.UndoReaction -> handleUndoReact(action)
is RoomDetailAction.UpdateQuickReactAction -> handleUpdateQuickReaction(action)
is RoomDetailAction.EnterRegularMode -> handleEnterRegularMode(action)
is RoomDetailAction.EnterEditMode -> handleEditAction(action)
is RoomDetailAction.EnterQuoteMode -> handleQuoteAction(action)
is RoomDetailAction.EnterReplyMode -> handleReplyAction(action)
is RoomDetailAction.DownloadOrOpen -> handleOpenOrDownloadFile(action)
is RoomDetailAction.NavigateToEvent -> handleNavigateToEvent(action)
is RoomDetailAction.HandleTombstoneEvent -> handleTombstoneEvent(action)
is RoomDetailAction.ResendMessage -> handleResendEvent(action)
is RoomDetailAction.RemoveFailedEcho -> handleRemove(action)
is RoomDetailAction.ResendAll -> handleResendAll()
is RoomDetailAction.MarkAllAsRead -> handleMarkAllAsRead()
is RoomDetailAction.ReportContent -> handleReportContent(action)
is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action)
is RoomDetailAction.UserIsTyping -> handleUserIsTyping(action)
is RoomDetailAction.SaveDraft -> handleSaveDraft(action)
is RoomDetailAction.SendMessage -> handleSendMessage(action)
is RoomDetailAction.SendMedia -> handleSendMedia(action)
is RoomDetailAction.SendSticker -> handleSendSticker(action)
is RoomDetailAction.TimelineEventTurnsVisible -> handleEventVisible(action)
is RoomDetailAction.TimelineEventTurnsInvisible -> handleEventInvisible(action)
is RoomDetailAction.LoadMoreTimelineEvents -> handleLoadMore(action)
is RoomDetailAction.SendReaction -> handleSendReaction(action)
is RoomDetailAction.AcceptInvite -> handleAcceptInvite()
is RoomDetailAction.RejectInvite -> handleRejectInvite()
is RoomDetailAction.RedactAction -> handleRedactEvent(action)
is RoomDetailAction.UndoReaction -> handleUndoReact(action)
is RoomDetailAction.UpdateQuickReactAction -> handleUpdateQuickReaction(action)
is RoomDetailAction.EnterRegularMode -> handleEnterRegularMode(action)
is RoomDetailAction.EnterEditMode -> handleEditAction(action)
is RoomDetailAction.EnterQuoteMode -> handleQuoteAction(action)
is RoomDetailAction.EnterReplyMode -> handleReplyAction(action)
is RoomDetailAction.DownloadOrOpen -> handleOpenOrDownloadFile(action)
is RoomDetailAction.NavigateToEvent -> handleNavigateToEvent(action)
is RoomDetailAction.HandleTombstoneEvent -> handleTombstoneEvent(action)
is RoomDetailAction.ResendMessage -> handleResendEvent(action)
is RoomDetailAction.RemoveFailedEcho -> handleRemove(action)
is RoomDetailAction.ResendAll -> handleResendAll()
is RoomDetailAction.MarkAllAsRead -> handleMarkAllAsRead()
is RoomDetailAction.ReportContent -> handleReportContent(action)
is RoomDetailAction.IgnoreUser -> handleIgnoreUser(action)
is RoomDetailAction.EnterTrackingUnreadMessagesState -> startTrackingUnreadMessages()
is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages()
is RoomDetailAction.ReplyToOptions -> handleReplyToOptions(action)
is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action)
is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action)
is RoomDetailAction.RequestVerification -> handleRequestVerification(action)
is RoomDetailAction.ResumeVerification -> handleResumeRequestVerification(action)
is RoomDetailAction.ReRequestKeys -> handleReRequestKeys(action)
is RoomDetailAction.TapOnFailedToDecrypt -> handleTapOnFailedToDecrypt(action)
is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment()
is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager()
is RoomDetailAction.StartCall -> handleStartCall(action)
is RoomDetailAction.EndCall -> handleEndCall()
is RoomDetailAction.ManageIntegrations -> handleManageIntegrations()
is RoomDetailAction.AddJitsiWidget -> handleAddJitsiConference(action)
is RoomDetailAction.RemoveWidget -> handleDeleteWidget(action.widgetId)
is RoomDetailAction.EnsureNativeWidgetAllowed -> handleCheckWidgetAllowed(action)
is RoomDetailAction.CancelSend -> handleCancel(action)
is RoomDetailAction.OpenOrCreateDm -> handleOpenOrCreateDm(action)
is RoomDetailAction.JumpToReadReceipt -> handleJumpToReadReceipt(action)
RoomDetailAction.QuickActionInvitePeople -> handleInvitePeople()
RoomDetailAction.QuickActionSetAvatar -> handleQuickSetAvatar()
is RoomDetailAction.SetAvatarAction -> handleSetNewAvatar(action)
RoomDetailAction.QuickActionSetTopic -> _viewEvents.post(RoomDetailViewEvents.OpenRoomSettings)
is RoomDetailAction.ShowRoomAvatarFullScreen -> {
is RoomDetailAction.ExitTrackingUnreadMessagesState -> stopTrackingUnreadMessages()
is RoomDetailAction.ReplyToOptions -> handleReplyToOptions(action)
is RoomDetailAction.AcceptVerificationRequest -> handleAcceptVerification(action)
is RoomDetailAction.DeclineVerificationRequest -> handleDeclineVerification(action)
is RoomDetailAction.RequestVerification -> handleRequestVerification(action)
is RoomDetailAction.ResumeVerification -> handleResumeRequestVerification(action)
is RoomDetailAction.ReRequestKeys -> handleReRequestKeys(action)
is RoomDetailAction.TapOnFailedToDecrypt -> handleTapOnFailedToDecrypt(action)
is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment()
is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager()
is RoomDetailAction.StartCall -> handleStartCall(action)
is RoomDetailAction.EndCall -> handleEndCall()
is RoomDetailAction.ManageIntegrations -> handleManageIntegrations()
is RoomDetailAction.AddJitsiWidget -> handleAddJitsiConference(action)
is RoomDetailAction.RemoveWidget -> handleDeleteWidget(action.widgetId)
is RoomDetailAction.EnsureNativeWidgetAllowed -> handleCheckWidgetAllowed(action)
is RoomDetailAction.CancelSend -> handleCancel(action)
is RoomDetailAction.OpenOrCreateDm -> handleOpenOrCreateDm(action)
is RoomDetailAction.JumpToReadReceipt -> handleJumpToReadReceipt(action)
RoomDetailAction.QuickActionInvitePeople -> handleInvitePeople()
RoomDetailAction.QuickActionSetAvatar -> handleQuickSetAvatar()
is RoomDetailAction.SetAvatarAction -> handleSetNewAvatar(action)
RoomDetailAction.QuickActionSetTopic -> _viewEvents.post(RoomDetailViewEvents.OpenRoomSettings)
is RoomDetailAction.ShowRoomAvatarFullScreen -> {
_viewEvents.post(
RoomDetailViewEvents.ShowRoomAvatarFullScreen(action.matrixItem, action.transitionView)
)
@ -549,7 +550,7 @@ class RoomDetailViewModel @AssistedInject constructor(
SendMode.EDIT(timelineEvent, currentDraft.text)
}
}
else -> null
else -> null
} ?: SendMode.REGULAR("", fromSharing = false)
)
}
@ -592,16 +593,16 @@ class RoomDetailViewModel @AssistedInject constructor(
return@withState false
}
when (itemId) {
R.id.resend_all -> state.asyncRoomSummary()?.hasFailedSending == true
R.id.timeline_setting -> true
R.id.invite -> state.canInvite
R.id.clear_all -> state.asyncRoomSummary()?.hasFailedSending == true
R.id.open_matrix_apps -> true
R.id.resend_all -> state.asyncRoomSummary()?.hasFailedSending == true
R.id.timeline_setting -> true
R.id.invite -> state.canInvite
R.id.clear_all -> state.asyncRoomSummary()?.hasFailedSending == true
R.id.open_matrix_apps -> true
R.id.voice_call,
R.id.video_call -> true // always show for discoverability
R.id.hangup_call -> webRtcPeerConnectionManager.currentCall != null
R.id.search -> true
else -> false
R.id.video_call -> true // always show for discoverability
R.id.hangup_call -> webRtcPeerConnectionManager.currentCall != null
R.id.search -> true
else -> false
}
}
@ -714,6 +715,11 @@ class RoomDetailViewModel @AssistedInject constructor(
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
popDraft()
}
is ParsedCommand.Confetti -> {
room.sendTextMessage(slashCommandResult.message, MessageType.MSGTYPE_CONFETTI)
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
popDraft()
}
is ParsedCommand.SendPoll -> {
room.sendPoll(slashCommandResult.question, slashCommandResult.options.mapIndexed { index, s -> OptionItem(s, "$index. $s") })
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
@ -742,7 +748,7 @@ class RoomDetailViewModel @AssistedInject constructor(
}
}.exhaustive
}
is SendMode.EDIT -> {
is SendMode.EDIT -> {
// is original event a reply?
val inReplyTo = state.sendMode.timelineEvent.root.getClearContent().toModel<MessageContent>()?.relatesTo?.inReplyTo?.eventId
?: state.sendMode.timelineEvent.root.content.toModel<EncryptedEventContent>()?.relatesTo?.inReplyTo?.eventId
@ -768,7 +774,7 @@ class RoomDetailViewModel @AssistedInject constructor(
_viewEvents.post(RoomDetailViewEvents.MessageSent)
popDraft()
}
is SendMode.QUOTE -> {
is SendMode.QUOTE -> {
val messageContent: MessageContent? =
state.sendMode.timelineEvent.annotations?.editSummary?.aggregatedContent.toModel()
?: state.sendMode.timelineEvent.root.getClearContent().toModel()
@ -791,7 +797,7 @@ class RoomDetailViewModel @AssistedInject constructor(
_viewEvents.post(RoomDetailViewEvents.MessageSent)
popDraft()
}
is SendMode.REPLY -> {
is SendMode.REPLY -> {
state.sendMode.timelineEvent.let {
room.replyToMessage(it, action.text.toString(), action.autoMarkdown)
_viewEvents.post(RoomDetailViewEvents.MessageSent)
@ -983,9 +989,29 @@ class RoomDetailViewModel @AssistedInject constructor(
visibleEventsObservable.accept(RoomDetailAction.TimelineEventTurnsVisible(event))
}
}
// handle chat effects here
if (vectorPreferences.chatEffectsEnabled()) {
chatEffectManager.checkForEffect(action.event)
}
}
}
override fun shouldStartEffect(effect: ChatEffect) {
when (effect) {
ChatEffect.CONFETTI -> {
_viewEvents.post(RoomDetailViewEvents.StartChatEffect(ChatEffect.CONFETTI))
}
ChatEffect.SNOW -> {
_viewEvents.post(RoomDetailViewEvents.StartChatEffect(ChatEffect.SNOW))
}
}
}
override fun stopEffects() {
_viewEvents.post(RoomDetailViewEvents.StopChatEffects)
}
private fun handleLoadMore(action: RoomDetailAction.LoadMoreTimelineEvents) {
timeline.paginate(action.direction, PAGINATION_COUNT)
}
@ -1387,6 +1413,7 @@ class RoomDetailViewModel @AssistedInject constructor(
if (vectorPreferences.sendTypingNotifs()) {
room.userStopsTyping()
}
chatEffectManager.dispose()
super.onCleared()
}
}

View File

@ -97,6 +97,7 @@ class VectorPreferences @Inject constructor(private val context: Context) {
private const val SETTINGS_SHOW_AVATAR_DISPLAY_NAME_CHANGES_MESSAGES_KEY = "SETTINGS_SHOW_AVATAR_DISPLAY_NAME_CHANGES_MESSAGES_KEY"
private const val SETTINGS_VIBRATE_ON_MENTION_KEY = "SETTINGS_VIBRATE_ON_MENTION_KEY"
private const val SETTINGS_SEND_MESSAGE_WITH_ENTER = "SETTINGS_SEND_MESSAGE_WITH_ENTER"
private const val SETTINGS_ENABLE_CHAT_EFFECTS = "SETTINGS_ENABLE_CHAT_EFFECTS"
// Help
private const val SETTINGS_SHOULD_SHOW_HELP_ON_ROOM_LIST_KEY = "SETTINGS_SHOULD_SHOW_HELP_ON_ROOM_LIST_KEY"
@ -869,6 +870,10 @@ class VectorPreferences @Inject constructor(private val context: Context) {
return defaultPrefs.getBoolean(SETTINGS_SECURITY_USE_GRACE_PERIOD_FLAG, true)
}
fun chatEffectsEnabled(): Boolean {
return defaultPrefs.getBoolean(SETTINGS_ENABLE_CHAT_EFFECTS, true)
}
/**
* Return true if Pin code is disabled, or if user set the settings to see full notification content
*/

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="106dp"
android:height="106dp"
android:viewportWidth="106"
android:viewportHeight="106">
<path
android:pathData="M53,53m-52,0a52,52 0,1 1,104 0a52,52 0,1 1,-104 0"
android:strokeAlpha="0.5168103"
android:strokeWidth="1"
android:fillColor="#FFFFFF"
android:strokeColor="#979797"
android:fillType="evenOdd"/>
</vector>

View File

@ -6,6 +6,21 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<nl.dionsegijn.konfetti.KonfettiView
android:id="@+id/viewKonfetti"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:elevation="4dp"
android:visibility="invisible" />
<com.jetradarmobile.snowfall.SnowfallView
android:id="@+id/viewSnowFall"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?vctr_chat_effect_snow_background"
android:elevation="4dp"
android:visibility="invisible" />
<androidx.appcompat.widget.Toolbar
android:id="@+id/roomToolbar"
style="@style/VectorToolbarStyle"

View File

@ -47,6 +47,7 @@
<attr name="vctr_social_login_button_twitter_style" format="reference" />
<attr name="vctr_social_login_button_apple_style" format="reference" />
<attr name="vctr_chat_effect_snow_background" format="color" />
</declare-styleable>
<declare-styleable name="PollResultLineView">

View File

@ -877,6 +877,8 @@
<string name="settings_show_read_receipts">Show read receipts</string>
<string name="settings_show_read_receipts_summary">Click on the read receipts for a detailed list.</string>
<string name="settings_show_room_member_state_events">Show room member state events</string>
<string name="settings_chat_effects_title">Show chat effects</string>
<string name="settings_chat_effects_description">Use /confetti command or send a message containing ❄️ or 🎉</string>
<string name="settings_show_room_member_state_events_summary">Includes invite/join/left/kick/ban events and avatar/display name changes.</string>
<string name="settings_show_join_leave_messages">Show join and leave events</string>
<string name="settings_show_join_leave_messages_summary">Invites, kicks, and bans are unaffected.</string>
@ -2568,6 +2570,8 @@
<item quantity="other">Show %d devices you can verify with now</item>
</plurals>
<string name="command_confetti">Sends the given message with confetti</string>
<string name="unencrypted">Unencrypted</string>
<string name="encrypted_unverified">Encrypted by an unverified device</string>
<string name="review_logins">Review where youre logged in</string>

View File

@ -200,6 +200,9 @@
<item name="vctr_social_login_button_facebook_style">@style/WidgetButtonSocialLogin.Facebook.Dark</item>
<item name="vctr_social_login_button_twitter_style">@style/WidgetButtonSocialLogin.Twitter.Dark</item>
<item name="vctr_social_login_button_apple_style">@style/WidgetButtonSocialLogin.Apple.Dark</item>
<!-- chat effect -->
<item name="vctr_chat_effect_snow_background">@android:color/transparent</item>
</style>
<style name="AppTheme.Dark" parent="AppTheme.Base.Dark" />

View File

@ -197,12 +197,14 @@
<item name="android:windowSharedElementEnterTransition">@transition/image_preview_transition</item>
<item name="android:windowSharedElementExitTransition">@transition/image_preview_transition</item>
<item name="vctr_social_login_button_google_style">@style/WidgetButtonSocialLogin.Google.Light</item>
<item name="vctr_social_login_button_github_style">@style/WidgetButtonSocialLogin.Github.Light</item>
<item name="vctr_social_login_button_facebook_style">@style/WidgetButtonSocialLogin.Facebook.Light</item>
<item name="vctr_social_login_button_twitter_style">@style/WidgetButtonSocialLogin.Twitter.Light</item>
<item name="vctr_social_login_button_apple_style">@style/WidgetButtonSocialLogin.Apple.Light</item>
<!-- chat effect -->
<item name="vctr_chat_effect_snow_background">@color/black_alpha</item>
</style>
<style name="AppTheme.Light" parent="AppTheme.Base.Light" />

View File

@ -86,6 +86,12 @@
android:summary="@string/settings_show_room_member_state_events_summary"
android:title="@string/settings_show_room_member_state_events" />
<im.vector.app.core.preference.VectorSwitchPreference
android:defaultValue="true"
android:key="SETTINGS_ENABLE_CHAT_EFFECTS"
android:summary="@string/settings_chat_effects_description"
android:title="@string/settings_chat_effects_title" />
<im.vector.app.core.preference.VectorSwitchPreference
android:defaultValue="true"
android:key="SETTINGS_SHOW_JOIN_LEAVE_MESSAGES_KEY"