mirror of
https://github.com/vector-im/element-android.git
synced 2024-11-15 01:35:07 +08:00
commit
d7feb6dd5c
@ -10,7 +10,7 @@ Improvements 🙌:
|
||||
- Hide the algorithm when turning on e2e (#897)
|
||||
|
||||
Other changes:
|
||||
-
|
||||
- Add support for /rainbow and /rainbowme commands (#879)
|
||||
|
||||
Bugfix 🐛:
|
||||
-
|
||||
|
@ -113,3 +113,39 @@ fun containsOnlyEmojis(str: String?): Boolean {
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as split, but considering emojis
|
||||
*/
|
||||
fun CharSequence.splitEmoji(): List<CharSequence> {
|
||||
val result = mutableListOf<CharSequence>()
|
||||
|
||||
var index = 0
|
||||
|
||||
while (index < length) {
|
||||
val firstChar = get(index)
|
||||
|
||||
if (firstChar.toInt() == 0x200e) {
|
||||
// Left to right mark. What should I do with it?
|
||||
} else if (firstChar.toInt() in 0xD800..0xDBFF && index + 1 < length) {
|
||||
// We have the start of a surrogate pair
|
||||
val secondChar = get(index + 1)
|
||||
|
||||
if (secondChar.toInt() in 0xDC00..0xDFFF) {
|
||||
// We have an emoji
|
||||
result.add("$firstChar$secondChar")
|
||||
index++
|
||||
} else {
|
||||
// Not sure what we have here...
|
||||
result.add("$firstChar")
|
||||
}
|
||||
} else {
|
||||
// Regular char
|
||||
result.add("$firstChar")
|
||||
}
|
||||
|
||||
index++
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
@ -37,6 +37,8 @@ enum class Command(val command: String, val parameters: String, @StringRes val d
|
||||
KICK_USER("/kick", "<user-id> [reason]", R.string.command_description_kick_user),
|
||||
CHANGE_DISPLAY_NAME("/nick", "<display-name>", R.string.command_description_nick),
|
||||
MARKDOWN("/markdown", "<on|off>", R.string.command_description_markdown),
|
||||
RAINBOW("/rainbow", "<message>", R.string.command_description_rainbow),
|
||||
RAINBOW_EMOTE("/rainbowme", "<message>", R.string.command_description_rainbow_emote),
|
||||
CLEAR_SCALAR_TOKEN("/clear_scalar_token", "", R.string.command_description_clear_scalar_token),
|
||||
SPOILER("/spoiler", "<message>", R.string.command_description_spoiler);
|
||||
|
||||
|
@ -80,6 +80,16 @@ object CommandParser {
|
||||
|
||||
ParsedCommand.SendEmote(message)
|
||||
}
|
||||
Command.RAINBOW.command -> {
|
||||
val message = textMessage.subSequence(Command.RAINBOW.command.length, textMessage.length).trim()
|
||||
|
||||
ParsedCommand.SendRainbow(message)
|
||||
}
|
||||
Command.RAINBOW_EMOTE.command -> {
|
||||
val message = textMessage.subSequence(Command.RAINBOW_EMOTE.command.length, textMessage.length).trim()
|
||||
|
||||
ParsedCommand.SendRainbowEmote(message)
|
||||
}
|
||||
Command.JOIN_ROOM.command -> {
|
||||
if (messageParts.size >= 2) {
|
||||
val roomAlias = messageParts[1]
|
||||
|
@ -34,6 +34,8 @@ sealed class ParsedCommand {
|
||||
// Valid commands:
|
||||
|
||||
class SendEmote(val message: CharSequence) : ParsedCommand()
|
||||
class SendRainbow(val message: CharSequence) : ParsedCommand()
|
||||
class SendRainbowEmote(val message: CharSequence) : ParsedCommand()
|
||||
class BanUser(val userId: String, val reason: String?) : ParsedCommand()
|
||||
class UnbanUser(val userId: String, val reason: String?) : ParsedCommand()
|
||||
class SetUserPowerLevel(val userId: String, val powerLevel: Int) : ParsedCommand()
|
||||
|
@ -56,6 +56,7 @@ import im.vector.matrix.android.internal.crypto.model.event.EncryptedEventConten
|
||||
import im.vector.matrix.rx.rx
|
||||
import im.vector.matrix.rx.unwrap
|
||||
import im.vector.riotx.R
|
||||
import im.vector.riotx.core.extensions.exhaustive
|
||||
import im.vector.riotx.core.extensions.postLiveEvent
|
||||
import im.vector.riotx.core.platform.VectorViewModel
|
||||
import im.vector.riotx.core.resources.StringProvider
|
||||
@ -64,6 +65,7 @@ import im.vector.riotx.core.utils.LiveEvent
|
||||
import im.vector.riotx.core.utils.subscribeLogError
|
||||
import im.vector.riotx.features.command.CommandParser
|
||||
import im.vector.riotx.features.command.ParsedCommand
|
||||
import im.vector.riotx.features.home.room.detail.composer.rainbow.RainbowGenerator
|
||||
import im.vector.riotx.features.home.room.detail.timeline.helper.TimelineDisplayableEvents
|
||||
import im.vector.riotx.features.home.room.typing.TypingHelper
|
||||
import im.vector.riotx.features.settings.VectorPreferences
|
||||
@ -83,6 +85,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||
private val vectorPreferences: VectorPreferences,
|
||||
private val stringProvider: StringProvider,
|
||||
private val typingHelper: TypingHelper,
|
||||
private val rainbowGenerator: RainbowGenerator,
|
||||
private val session: Session
|
||||
) : VectorViewModel<RoomDetailViewState, RoomDetailAction, RoomDetailViewEvents>(initialState), Timeline.Listener {
|
||||
|
||||
@ -385,6 +388,20 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled())
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.SendRainbow -> {
|
||||
slashCommandResult.message.toString().let {
|
||||
room.sendFormattedTextMessage(it, rainbowGenerator.generate(it))
|
||||
}
|
||||
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled())
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.SendRainbowEmote -> {
|
||||
slashCommandResult.message.toString().let {
|
||||
room.sendFormattedTextMessage(it, rainbowGenerator.generate(it), MessageType.MSGTYPE_EMOTE)
|
||||
}
|
||||
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandHandled())
|
||||
popDraft()
|
||||
}
|
||||
is ParsedCommand.SendSpoiler -> {
|
||||
room.sendFormattedTextMessage(
|
||||
"[${stringProvider.getString(R.string.spoiler)}](${slashCommandResult.message})",
|
||||
@ -401,7 +418,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||
// TODO
|
||||
_sendMessageResultLiveData.postLiveEvent(SendMessageResult.SlashCommandNotImplemented)
|
||||
}
|
||||
}
|
||||
}.exhaustive
|
||||
}
|
||||
is SendMode.EDIT -> {
|
||||
// is original event a reply?
|
||||
@ -459,7 +476,7 @@ class RoomDetailViewModel @AssistedInject constructor(@Assisted initialState: Ro
|
||||
popDraft()
|
||||
}
|
||||
}
|
||||
}
|
||||
}.exhaustive
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright 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.riotx.features.home.room.detail.composer.rainbow
|
||||
|
||||
import im.vector.riotx.core.utils.splitEmoji
|
||||
import javax.inject.Inject
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* Inspired from React-Sdk
|
||||
* Ref: https://github.com/matrix-org/matrix-react-sdk/blob/develop/src/utils/colour.js
|
||||
*/
|
||||
class RainbowGenerator @Inject constructor() {
|
||||
|
||||
fun generate(text: String): String {
|
||||
val split = text.splitEmoji()
|
||||
val frequency = 360f / split.size
|
||||
|
||||
return split
|
||||
.mapIndexed { idx, letter ->
|
||||
// Do better than React-Sdk: Avoid adding font color for spaces
|
||||
if (letter == " ") {
|
||||
"$letter"
|
||||
} else {
|
||||
val dashColor = hueToRGB(idx * frequency, 1.0f, 0.5f).toDashColor()
|
||||
"<font color=\"$dashColor\">$letter</font>"
|
||||
}
|
||||
}
|
||||
.joinToString(separator = "")
|
||||
}
|
||||
|
||||
private fun hueToRGB(h: Float, s: Float, l: Float): RgbColor {
|
||||
val c = s * (1 - abs(2 * l - 1))
|
||||
val x = c * (1 - abs((h / 60) % 2 - 1))
|
||||
val m = l - c / 2
|
||||
|
||||
var r = 0f
|
||||
var g = 0f
|
||||
var b = 0f
|
||||
|
||||
when {
|
||||
h < 60f -> {
|
||||
r = c
|
||||
g = x
|
||||
}
|
||||
h < 120f -> {
|
||||
r = x
|
||||
g = c
|
||||
}
|
||||
h < 180f -> {
|
||||
g = c
|
||||
b = x
|
||||
}
|
||||
h < 240f -> {
|
||||
g = x
|
||||
b = c
|
||||
}
|
||||
h < 300f -> {
|
||||
r = x
|
||||
b = c
|
||||
}
|
||||
else -> {
|
||||
r = c
|
||||
b = x
|
||||
}
|
||||
}
|
||||
|
||||
return RgbColor(
|
||||
((r + m) * 255).roundToInt(),
|
||||
((g + m) * 255).roundToInt(),
|
||||
((b + m) * 255).roundToInt()
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 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.riotx.features.home.room.detail.composer.rainbow
|
||||
|
||||
data class RgbColor(
|
||||
val r: Int,
|
||||
val g: Int,
|
||||
val b: Int
|
||||
)
|
||||
|
||||
fun RgbColor.toDashColor(): String {
|
||||
return listOf(r, g, b)
|
||||
.joinToString(separator = "", prefix = "#") {
|
||||
it.toString(16).padStart(2, '0')
|
||||
}
|
||||
}
|
@ -44,6 +44,9 @@
|
||||
<string name="room_list_sharing_header_recent_rooms">Recent rooms</string>
|
||||
<string name="room_list_sharing_header_other_rooms">Other rooms</string>
|
||||
|
||||
<string name="command_description_rainbow">Sends the given message colored as a rainbow</string>
|
||||
<string name="command_description_rainbow_emote">Sends the given emote colored as a rainbow</string>
|
||||
|
||||
<!-- Title for category in the settings which affect what is displayed in the timeline (ex: show read receipts, etc.) -->
|
||||
<string name="settings_category_timeline">Timeline</string>
|
||||
|
||||
|
@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Copyright 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.riotx.features.home.room.detail.composer.rainbow
|
||||
|
||||
import im.vector.riotx.test.trimIndentOneLine
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
@Suppress("SpellCheckingInspection")
|
||||
class RainbowGeneratorTest {
|
||||
|
||||
private val rainbowGenerator = RainbowGenerator()
|
||||
|
||||
@Test
|
||||
fun testEmpty() {
|
||||
assertEquals("", rainbowGenerator.generate(""))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAscii1() {
|
||||
assertEquals("""<font color="#ff0000">a</font>""", rainbowGenerator.generate("a"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAscii2() {
|
||||
val expected = """
|
||||
<font color="#ff0000">a</font>
|
||||
<font color="#00ffff">b</font>
|
||||
""".trimIndentOneLine()
|
||||
|
||||
assertEquals(expected, rainbowGenerator.generate("ab"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testAscii3() {
|
||||
val expected = """
|
||||
<font color="#ff0000">T</font>
|
||||
<font color="#ff5500">h</font>
|
||||
<font color="#ffaa00">i</font>
|
||||
<font color="#ffff00">s</font>
|
||||
|
||||
<font color="#55ff00">i</font>
|
||||
<font color="#00ff00">s</font>
|
||||
|
||||
<font color="#00ffaa">a</font>
|
||||
|
||||
<font color="#00aaff">r</font>
|
||||
<font color="#0055ff">a</font>
|
||||
<font color="#0000ff">i</font>
|
||||
<font color="#5500ff">n</font>
|
||||
<font color="#aa00ff">b</font>
|
||||
<font color="#ff00ff">o</font>
|
||||
<font color="#ff00aa">w</font>
|
||||
<font color="#ff0055">!</font>
|
||||
""".trimIndentOneLine()
|
||||
|
||||
assertEquals(expected, rainbowGenerator.generate("This is a rainbow!"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEmoji1() {
|
||||
assertEquals("""<font color="#ff0000">🤞</font>""", rainbowGenerator.generate("\uD83E\uDD1E")) // 🤞
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEmoji2() {
|
||||
assertEquals("""<font color="#ff0000">🤞</font>""", rainbowGenerator.generate("🤞"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEmoji3() {
|
||||
val expected = """
|
||||
<font color="#ff0000">🤞</font>
|
||||
<font color="#00ffff">🙂</font>
|
||||
""".trimIndentOneLine()
|
||||
|
||||
assertEquals(expected, rainbowGenerator.generate("🤞🙂"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEmojiMix1() {
|
||||
val expected = """
|
||||
<font color="#ff0000">H</font>
|
||||
<font color="#ff6d00">e</font>
|
||||
<font color="#ffdb00">l</font>
|
||||
<font color="#b6ff00">l</font>
|
||||
<font color="#49ff00">o</font>
|
||||
|
||||
<font color="#00ff92">🤞</font>
|
||||
|
||||
<font color="#0092ff">w</font>
|
||||
<font color="#0024ff">o</font>
|
||||
<font color="#4900ff">r</font>
|
||||
<font color="#b600ff">l</font>
|
||||
<font color="#ff00db">d</font>
|
||||
<font color="#ff006d">!</font>
|
||||
""".trimIndentOneLine()
|
||||
|
||||
assertEquals(expected, rainbowGenerator.generate("Hello 🤞 world!"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEmojiMix2() {
|
||||
val expected = """
|
||||
<font color="#ff0000">a</font>
|
||||
<font color="#00ffff">🤞</font>
|
||||
""".trimIndentOneLine()
|
||||
|
||||
assertEquals(expected, rainbowGenerator.generate("a🤞"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testEmojiMix3() {
|
||||
val expected = """
|
||||
<font color="#ff0000">🤞</font>
|
||||
<font color="#00ffff">a</font>
|
||||
""".trimIndentOneLine()
|
||||
|
||||
assertEquals(expected, rainbowGenerator.generate("🤞a"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testError1() {
|
||||
assertEquals("<font color=\"#ff0000\">\uD83E</font>", rainbowGenerator.generate("\uD83E"))
|
||||
}
|
||||
}
|
19
vector/src/test/java/im/vector/riotx/test/Extensions.kt
Normal file
19
vector/src/test/java/im/vector/riotx/test/Extensions.kt
Normal file
@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 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.riotx.test
|
||||
|
||||
fun String.trimIndentOneLine() = trimIndent().replace("\n", "")
|
Loading…
Reference in New Issue
Block a user