Merge pull request #2535 from vector-im/feature/bca/confetti
Chat Effects XMAS PR ❄️ 🎉
This commit is contained in:
commit
b4b302c1f2
|
@ -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)
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
"⛄️",
|
||||||
|
"☃️",
|
||||||
|
"❄️"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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(
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 |
|
@ -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>
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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 you’re logged in</string>
|
<string name="review_logins">Review where you’re logged in</string>
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
|
@ -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"
|
||||||
|
|
Loading…
Reference in New Issue