Merge pull request #913 from vector-im/feature/rainbow

Rainbow
This commit is contained in:
Benoit Marty 2020-01-29 16:35:34 +01:00 committed by GitHub
commit d7feb6dd5c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 351 additions and 3 deletions

View File

@ -10,7 +10,7 @@ Improvements 🙌:
- Hide the algorithm when turning on e2e (#897)
Other changes:
-
- Add support for /rainbow and /rainbowme commands (#879)
Bugfix 🐛:
-

View File

@ -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
}

View File

@ -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);

View File

@ -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]

View File

@ -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()

View File

@ -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
}
}

View File

@ -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()
)
}
}

View File

@ -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')
}
}

View File

@ -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>

View File

@ -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"))
}
}

View 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", "")