Merge pull request #2535 from vector-im/feature/bca/confetti

Chat Effects XMAS PR ❄️ 🎉
This commit is contained in:
Benoit Marty 2020-12-15 11:41:29 +01:00 committed by GitHub
commit b4b302c1f2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 287 additions and 14 deletions

View File

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

View File

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

View File

@ -33,4 +33,7 @@ object MessageType {
// Add, in local, a fake message type in order to StickerMessage can inherit Message class // 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 // 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_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 // Badge for compatibility
implementation 'me.leolin:ShortcutBadger:1.1.22@aar' 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 // DI
implementation "com.google.dagger:dagger:$daggerVersion" implementation "com.google.dagger:dagger:$daggerVersion"
kapt "com.google.dagger:dagger-compiler:$daggerVersion" kapt "com.google.dagger:dagger-compiler:$daggerVersion"

View File

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

View File

@ -44,7 +44,9 @@ enum class Command(val command: String, val parameters: String, @StringRes val d
POLL("/poll", "Question | Option 1 | Option 2 ...", R.string.command_description_poll), POLL("/poll", "Question | Option 1 | Option 2 ...", R.string.command_description_poll),
SHRUG("/shrug", "<message>", R.string.command_description_shrug), SHRUG("/shrug", "<message>", R.string.command_description_shrug),
PLAIN("/plain", "<message>", R.string.command_description_plain), 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),
SNOW("/snow", "<message>", R.string.command_snow);
val length val length
get() = command.length + 1 get() = command.length + 1

View File

@ -18,6 +18,7 @@ package im.vector.app.features.command
import im.vector.app.core.extensions.isEmail import im.vector.app.core.extensions.isEmail
import im.vector.app.core.extensions.isMsisdn import im.vector.app.core.extensions.isMsisdn
import im.vector.app.features.home.room.detail.ChatEffect
import org.matrix.android.sdk.api.MatrixPatterns import org.matrix.android.sdk.api.MatrixPatterns
import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.identity.ThreePid
import timber.log.Timber import timber.log.Timber
@ -291,6 +292,14 @@ object CommandParser {
Command.DISCARD_SESSION.command -> { Command.DISCARD_SESSION.command -> {
ParsedCommand.DiscardSession ParsedCommand.DiscardSession
} }
Command.CONFETTI.command -> {
val message = textMessage.substring(Command.CONFETTI.command.length).trim()
ParsedCommand.SendChatEffect(ChatEffect.CONFETTI, message)
}
Command.SNOW.command -> {
val message = textMessage.substring(Command.SNOW.command.length).trim()
ParsedCommand.SendChatEffect(ChatEffect.SNOW, message)
}
else -> { else -> {
// Unknown command // Unknown command
ParsedCommand.ErrorUnknownSlashCommand(slashCommand) ParsedCommand.ErrorUnknownSlashCommand(slashCommand)

View File

@ -16,6 +16,7 @@
package im.vector.app.features.command package im.vector.app.features.command
import im.vector.app.features.home.room.detail.ChatEffect
import org.matrix.android.sdk.api.session.identity.ThreePid import org.matrix.android.sdk.api.session.identity.ThreePid
/** /**
@ -55,4 +56,5 @@ sealed class ParsedCommand {
class SendShrug(val message: CharSequence) : ParsedCommand() class SendShrug(val message: CharSequence) : ParsedCommand()
class SendPoll(val question: String, val options: List<String>) : ParsedCommand() class SendPoll(val question: String, val options: List<String>) : ParsedCommand()
object DiscardSession : ParsedCommand() object DiscardSession : ParsedCommand()
class SendChatEffect(val chatEffect: ChatEffect, val message: String) : ParsedCommand()
} }

View File

@ -0,0 +1,141 @@
/*
* 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
}
fun ChatEffect.toMessageType(): String {
return when (this) {
ChatEffect.CONFETTI -> MessageType.MSGTYPE_CONFETTI
ChatEffect.SNOW -> MessageType.MSGTYPE_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
* precisely 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 = mutableListOf<String>()
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
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 -> {
event.root.getClearContent().toModel<MessageContent>()?.body
?.let { text ->
when {
EMOJIS_FOR_CONFETTI.any { text.contains(it) } -> ChatEffect.CONFETTI
EMOJIS_FOR_SNOW.any { text.contains(it) } -> ChatEffect.SNOW
else -> null
}
}
}
else -> null
}
}
companion object {
private val EMOJIS_FOR_CONFETTI = listOf(
"🎉",
"🎊"
)
private val EMOJIS_FOR_SNOW = listOf(
"⛄️",
"☃️",
"❄️"
)
}
}

View File

@ -21,6 +21,7 @@ import android.app.Activity
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Color
import android.graphics.Typeface import android.graphics.Typeface
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
@ -48,11 +49,13 @@ import androidx.core.text.toSpannable
import androidx.core.util.Pair import androidx.core.util.Pair
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.forEach import androidx.core.view.forEach
import androidx.core.view.isInvisible
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.transition.TransitionManager
import com.airbnb.epoxy.EpoxyModel import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.OnModelBuildFinishedListener import com.airbnb.epoxy.OnModelBuildFinishedListener
import com.airbnb.epoxy.addGlidePreloader import com.airbnb.epoxy.addGlidePreloader
@ -168,6 +171,8 @@ import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_room_detail.* import kotlinx.android.synthetic.main.fragment_room_detail.*
import kotlinx.android.synthetic.main.composer_layout.view.* import kotlinx.android.synthetic.main.composer_layout.view.*
import kotlinx.android.synthetic.main.merge_overlay_waiting_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.billcarsonfr.jsonviewer.JSonViewerDialog
import org.commonmark.parser.Parser import org.commonmark.parser.Parser
import org.matrix.android.sdk.api.MatrixCallback import org.matrix.android.sdk.api.MatrixCallback
@ -378,6 +383,8 @@ class RoomDetailFragment @Inject constructor(
is RoomDetailViewEvents.ShowRoomAvatarFullScreen -> it.matrixItem?.let { item -> is RoomDetailViewEvents.ShowRoomAvatarFullScreen -> it.matrixItem?.let { item ->
navigator.openBigImageViewer(requireActivity(), it.view, item) navigator.openBigImageViewer(requireActivity(), it.view, item)
} }
is RoomDetailViewEvents.StartChatEffect -> handleChatEffect(it.type)
RoomDetailViewEvents.StopChatEffects -> handleStopChatEffects()
}.exhaustive }.exhaustive
} }
@ -386,6 +393,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?) { override fun onImageReady(uri: Uri?) {
uri ?: return uri ?: return
roomDetailViewModel.handle( roomDetailViewModel.handle(

View File

@ -95,4 +95,7 @@ sealed class RoomDetailViewEvents : VectorViewEvents {
// TODO Remove // TODO Remove
object SlashCommandNotImplemented : SendMessageResult() 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 org.matrix.android.sdk.rx.unwrap
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.lang.Exception
import java.util.UUID import java.util.UUID
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@ -115,8 +114,9 @@ class RoomDetailViewModel @AssistedInject constructor(
private val roomSummaryHolder: RoomSummaryHolder, private val roomSummaryHolder: RoomSummaryHolder,
private val typingHelper: TypingHelper, private val typingHelper: TypingHelper,
private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager, private val webRtcPeerConnectionManager: WebRtcPeerConnectionManager,
private val chatEffectManager: ChatEffectManager,
timelineSettingsFactory: TimelineSettingsFactory 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 room = session.getRoom(initialState.roomId)!!
private val eventId = initialState.eventId private val eventId = initialState.eventId
@ -171,6 +171,7 @@ class RoomDetailViewModel @AssistedInject constructor(
room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear() room.rx().loadRoomMembersIfNeeded().subscribeLogError().disposeOnClear()
// Inform the SDK that the room is displayed // Inform the SDK that the room is displayed
session.onRoomDisplayed(initialState.roomId) session.onRoomDisplayed(initialState.roomId)
chatEffectManager.delegate = this
} }
private fun observePowerLevel() { private fun observePowerLevel() {
@ -549,7 +550,7 @@ class RoomDetailViewModel @AssistedInject constructor(
SendMode.EDIT(timelineEvent, currentDraft.text) SendMode.EDIT(timelineEvent, currentDraft.text)
} }
} }
else -> null else -> null
} ?: SendMode.REGULAR("", fromSharing = false) } ?: SendMode.REGULAR("", fromSharing = false)
) )
} }
@ -592,16 +593,16 @@ class RoomDetailViewModel @AssistedInject constructor(
return@withState false return@withState false
} }
when (itemId) { when (itemId) {
R.id.resend_all -> state.asyncRoomSummary()?.hasFailedSending == true R.id.resend_all -> state.asyncRoomSummary()?.hasFailedSending == true
R.id.timeline_setting -> true R.id.timeline_setting -> true
R.id.invite -> state.canInvite R.id.invite -> state.canInvite
R.id.clear_all -> state.asyncRoomSummary()?.hasFailedSending == true R.id.clear_all -> state.asyncRoomSummary()?.hasFailedSending == true
R.id.open_matrix_apps -> true R.id.open_matrix_apps -> true
R.id.voice_call, R.id.voice_call,
R.id.video_call -> true // always show for discoverability R.id.video_call -> true // always show for discoverability
R.id.hangup_call -> webRtcPeerConnectionManager.currentCall != null R.id.hangup_call -> webRtcPeerConnectionManager.currentCall != null
R.id.search -> true R.id.search -> true
else -> false else -> false
} }
} }
@ -714,6 +715,11 @@ class RoomDetailViewModel @AssistedInject constructor(
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
popDraft() popDraft()
} }
is ParsedCommand.SendChatEffect -> {
room.sendTextMessage(slashCommandResult.message, slashCommandResult.chatEffect.toMessageType())
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
popDraft()
}
is ParsedCommand.SendPoll -> { is ParsedCommand.SendPoll -> {
room.sendPoll(slashCommandResult.question, slashCommandResult.options.mapIndexed { index, s -> OptionItem(s, "$index. $s") }) room.sendPoll(slashCommandResult.question, slashCommandResult.options.mapIndexed { index, s -> OptionItem(s, "$index. $s") })
_viewEvents.post(RoomDetailViewEvents.SlashCommandHandled()) _viewEvents.post(RoomDetailViewEvents.SlashCommandHandled())
@ -983,9 +989,22 @@ class RoomDetailViewModel @AssistedInject constructor(
visibleEventsObservable.accept(RoomDetailAction.TimelineEventTurnsVisible(event)) visibleEventsObservable.accept(RoomDetailAction.TimelineEventTurnsVisible(event))
} }
} }
// handle chat effects here
if (vectorPreferences.chatEffectsEnabled()) {
chatEffectManager.checkForEffect(action.event)
}
} }
} }
override fun shouldStartEffect(effect: ChatEffect) {
_viewEvents.post(RoomDetailViewEvents.StartChatEffect(effect))
}
override fun stopEffects() {
_viewEvents.post(RoomDetailViewEvents.StopChatEffects)
}
private fun handleLoadMore(action: RoomDetailAction.LoadMoreTimelineEvents) { private fun handleLoadMore(action: RoomDetailAction.LoadMoreTimelineEvents) {
timeline.paginate(action.direction, PAGINATION_COUNT) timeline.paginate(action.direction, PAGINATION_COUNT)
} }
@ -1387,6 +1406,8 @@ class RoomDetailViewModel @AssistedInject constructor(
if (vectorPreferences.sendTypingNotifs()) { if (vectorPreferences.sendTypingNotifs()) {
room.userStopsTyping() room.userStopsTyping()
} }
chatEffectManager.delegate = null
chatEffectManager.dispose()
super.onCleared() 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_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_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_SEND_MESSAGE_WITH_ENTER = "SETTINGS_SEND_MESSAGE_WITH_ENTER"
private const val SETTINGS_ENABLE_CHAT_EFFECTS = "SETTINGS_ENABLE_CHAT_EFFECTS"
// Help // Help
private const val SETTINGS_SHOULD_SHOW_HELP_ON_ROOM_LIST_KEY = "SETTINGS_SHOULD_SHOW_HELP_ON_ROOM_LIST_KEY" 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) 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 * 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

@ -225,4 +225,17 @@
app:maxImageSize="16dp" app:maxImageSize="16dp"
app:tint="@color/black" /> app:tint="@color/black" />
<nl.dionsegijn.konfetti.KonfettiView
android:id="@+id/viewKonfetti"
android:layout_width="match_parent"
android:layout_height="match_parent"
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:visibility="invisible" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -47,6 +47,7 @@
<attr name="vctr_social_login_button_twitter_style" format="reference" /> <attr name="vctr_social_login_button_twitter_style" format="reference" />
<attr name="vctr_social_login_button_apple_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>
<declare-styleable name="PollResultLineView"> <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">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_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_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_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">Show join and leave events</string>
<string name="settings_show_join_leave_messages_summary">Invites, kicks, and bans are unaffected.</string> <string name="settings_show_join_leave_messages_summary">Invites, kicks, and bans are unaffected.</string>
@ -2568,6 +2570,9 @@
<item quantity="other">Show %d devices you can verify with now</item> <item quantity="other">Show %d devices you can verify with now</item>
</plurals> </plurals>
<string name="command_confetti">Sends the given message with confetti</string>
<string name="command_snow">Sends the given message with snow</string>
<string name="unencrypted">Unencrypted</string> <string name="unencrypted">Unencrypted</string>
<string name="encrypted_unverified">Encrypted by an unverified device</string> <string name="encrypted_unverified">Encrypted by an unverified device</string>
<string name="review_logins">Review where youre logged in</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_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_twitter_style">@style/WidgetButtonSocialLogin.Twitter.Dark</item>
<item name="vctr_social_login_button_apple_style">@style/WidgetButtonSocialLogin.Apple.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>
<style name="AppTheme.Dark" parent="AppTheme.Base.Dark" /> <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:windowSharedElementEnterTransition">@transition/image_preview_transition</item>
<item name="android:windowSharedElementExitTransition">@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_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_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_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_twitter_style">@style/WidgetButtonSocialLogin.Twitter.Light</item>
<item name="vctr_social_login_button_apple_style">@style/WidgetButtonSocialLogin.Apple.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>
<style name="AppTheme.Light" parent="AppTheme.Base.Light" /> <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:summary="@string/settings_show_room_member_state_events_summary"
android:title="@string/settings_show_room_member_state_events" /> 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 <im.vector.app.core.preference.VectorSwitchPreference
android:defaultValue="true" android:defaultValue="true"
android:key="SETTINGS_SHOW_JOIN_LEAVE_MESSAGES_KEY" android:key="SETTINGS_SHOW_JOIN_LEAVE_MESSAGES_KEY"