replace HtmlUtils with HtmlCompat (#1741)
* replace HtmlUtils with HtmlCompat * fix tests
This commit is contained in:
parent
c7e7da9433
commit
2ed14d0b90
@ -22,6 +22,7 @@ import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.text.HtmlCompat;
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
@ -42,7 +43,6 @@ import com.keylesspalace.tusky.entity.EmojiReaction;
|
||||
import com.keylesspalace.tusky.interfaces.StatusActionListener;
|
||||
import com.keylesspalace.tusky.util.CardViewMode;
|
||||
import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
||||
import com.keylesspalace.tusky.util.HtmlUtils;
|
||||
import com.keylesspalace.tusky.util.ImageLoadingHelper;
|
||||
import com.keylesspalace.tusky.util.LinkHelper;
|
||||
import com.keylesspalace.tusky.util.StatusDisplayOptions;
|
||||
@ -946,7 +946,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||
protected CharSequence getFavsText(Context context, int count) {
|
||||
if (count > 0) {
|
||||
String countString = numberFormat.format(count);
|
||||
return HtmlUtils.fromHtml(context.getResources().getQuantityString(R.plurals.favs, count, countString));
|
||||
return HtmlCompat.fromHtml(context.getResources().getQuantityString(R.plurals.favs, count, countString), HtmlCompat.FROM_HTML_MODE_LEGACY);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
@ -955,7 +955,7 @@ public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||
protected CharSequence getReblogsText(Context context, int count) {
|
||||
if (count > 0) {
|
||||
String countString = numberFormat.format(count);
|
||||
return HtmlUtils.fromHtml(context.getResources().getQuantityString(R.plurals.reblogs, count, countString));
|
||||
return HtmlCompat.fromHtml(context.getResources().getQuantityString(R.plurals.reblogs, count, countString), HtmlCompat.FROM_HTML_MODE_LEGACY);
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
|
@ -16,6 +16,8 @@
|
||||
package com.keylesspalace.tusky.db
|
||||
|
||||
import android.text.Spanned
|
||||
import androidx.core.text.parseAsHtml
|
||||
import androidx.core.text.toHtml
|
||||
import androidx.room.TypeConverter
|
||||
import com.google.gson.GsonBuilder
|
||||
import com.google.gson.reflect.TypeToken
|
||||
@ -27,7 +29,6 @@ import com.keylesspalace.tusky.entity.Emoji
|
||||
import com.keylesspalace.tusky.entity.Poll
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.json.SpannedTypeAdapter
|
||||
import com.keylesspalace.tusky.util.HtmlUtils
|
||||
import java.net.URLDecoder
|
||||
import java.net.URLEncoder
|
||||
import java.util.*
|
||||
@ -128,7 +129,7 @@ class Converters {
|
||||
if(spanned == null) {
|
||||
return null
|
||||
}
|
||||
return HtmlUtils.toHtml(spanned)
|
||||
return spanned.toHtml()
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
|
@ -29,8 +29,6 @@ import com.keylesspalace.tusky.db.AppDatabase
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.network.TimelineCases
|
||||
import com.keylesspalace.tusky.network.TimelineCasesImpl
|
||||
import com.keylesspalace.tusky.util.HtmlConverter
|
||||
import com.keylesspalace.tusky.util.HtmlConverterImpl
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import javax.inject.Singleton
|
||||
|
@ -6,17 +6,18 @@ import com.keylesspalace.tusky.db.AppDatabase
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.repository.TimelineRepository
|
||||
import com.keylesspalace.tusky.repository.TimelineRepositoryImpl
|
||||
import com.keylesspalace.tusky.util.HtmlConverter
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
|
||||
@Module
|
||||
class RepositoryModule {
|
||||
@Provides
|
||||
fun providesTimelineRepository(db: AppDatabase, mastodonApi: MastodonApi,
|
||||
accountManager: AccountManager, gson: Gson,
|
||||
htmlConverter: HtmlConverter): TimelineRepository {
|
||||
return TimelineRepositoryImpl(db.timelineDao(), mastodonApi, accountManager, gson,
|
||||
htmlConverter)
|
||||
fun providesTimelineRepository(
|
||||
db: AppDatabase,
|
||||
mastodonApi: MastodonApi,
|
||||
accountManager: AccountManager,
|
||||
gson: Gson
|
||||
): TimelineRepository {
|
||||
return TimelineRepositoryImpl(db.timelineDao(), mastodonApi, accountManager, gson)
|
||||
}
|
||||
}
|
@ -15,24 +15,16 @@
|
||||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import android.text.Spanned
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import com.keylesspalace.tusky.util.HtmlUtils
|
||||
import kotlinx.android.parcel.Parceler
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.android.parcel.WriteWith
|
||||
import java.util.*
|
||||
import java.util.Date
|
||||
|
||||
@Parcelize
|
||||
data class Account(
|
||||
val id: String,
|
||||
@SerializedName("username") val localUsername: String,
|
||||
@SerializedName("acct") val username: String,
|
||||
@SerializedName("display_name") val displayName: String?, // should never be null per Api definition, but some servers break the contract
|
||||
val note: @WriteWith<SpannedParceler>() Spanned,
|
||||
val note: Spanned,
|
||||
val url: String,
|
||||
val avatar: String,
|
||||
val header: String,
|
||||
@ -46,7 +38,7 @@ data class Account(
|
||||
val fields: List<Field>? = emptyList(), //nullable for backward compatibility
|
||||
val moved: Account? = null,
|
||||
val pleroma: PleromaAccount? = null
|
||||
) : Parcelable {
|
||||
) {
|
||||
|
||||
val name: String
|
||||
get() = if (displayName.isNullOrEmpty()) {
|
||||
@ -87,32 +79,28 @@ data class Account(
|
||||
fun isRemote(): Boolean = this.username != this.localUsername
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class AccountSource(
|
||||
val privacy: Status.Visibility,
|
||||
val sensitive: Boolean,
|
||||
val note: String,
|
||||
val fields: List<StringField>?
|
||||
): Parcelable
|
||||
)
|
||||
|
||||
@Parcelize
|
||||
data class Field (
|
||||
val name: String,
|
||||
val value: @WriteWith<SpannedParceler>() Spanned,
|
||||
val value: Spanned,
|
||||
@SerializedName("verified_at") val verifiedAt: Date?
|
||||
): Parcelable
|
||||
)
|
||||
|
||||
@Parcelize
|
||||
data class StringField (
|
||||
val name: String,
|
||||
val value: String
|
||||
): Parcelable
|
||||
)
|
||||
|
||||
@Parcelize
|
||||
data class PleromaAccount(
|
||||
@SerializedName("is_moderator") val isModerator: Boolean? = null,
|
||||
@SerializedName("is_admin") val isAdmin: Boolean? = null
|
||||
): Parcelable
|
||||
)
|
||||
|
||||
object SpannedParceler : Parceler<Spanned> {
|
||||
override fun create(parcel: Parcel): Spanned = HtmlUtils.fromHtml(parcel.readString())
|
||||
|
@ -15,23 +15,19 @@
|
||||
|
||||
package com.keylesspalace.tusky.entity
|
||||
|
||||
import android.os.Parcelable
|
||||
import android.text.Spanned
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.android.parcel.WriteWith
|
||||
|
||||
@Parcelize
|
||||
data class Card(
|
||||
val url: String,
|
||||
val title: @WriteWith<SpannedParceler>() Spanned,
|
||||
val description: @WriteWith<SpannedParceler>() Spanned,
|
||||
val title: Spanned,
|
||||
val description: Spanned,
|
||||
@SerializedName("author_name") val authorName: String,
|
||||
val image: String,
|
||||
val type: String,
|
||||
val width: Int,
|
||||
val height: Int
|
||||
) : Parcelable {
|
||||
) {
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return url.hashCode()
|
||||
|
@ -2,6 +2,8 @@ package com.keylesspalace.tusky.repository
|
||||
|
||||
import android.text.Spanned
|
||||
import android.text.SpannedString
|
||||
import androidx.core.text.parseAsHtml
|
||||
import androidx.core.text.toHtml
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import com.keylesspalace.tusky.db.*
|
||||
@ -10,7 +12,6 @@ import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.repository.TimelineRequestMode.DISK
|
||||
import com.keylesspalace.tusky.repository.TimelineRequestMode.NETWORK
|
||||
import com.keylesspalace.tusky.util.Either
|
||||
import com.keylesspalace.tusky.util.HtmlConverter
|
||||
import com.keylesspalace.tusky.util.dec
|
||||
import com.keylesspalace.tusky.util.inc
|
||||
import com.keylesspalace.tusky.util.trimTrailingWhitespace
|
||||
@ -42,8 +43,7 @@ class TimelineRepositoryImpl(
|
||||
private val timelineDao: TimelineDao,
|
||||
private val mastodonApi: MastodonApi,
|
||||
private val accountManager: AccountManager,
|
||||
private val gson: Gson,
|
||||
private val htmlConverter: HtmlConverter
|
||||
private val gson: Gson
|
||||
) : TimelineRepository {
|
||||
|
||||
init {
|
||||
@ -153,7 +153,7 @@ class TimelineRepositoryImpl(
|
||||
|
||||
for (status in statuses) {
|
||||
timelineDao.insertInTransaction(
|
||||
status.toEntity(accountId, htmlConverter, gson),
|
||||
status.toEntity(accountId, gson),
|
||||
status.account.toEntity(accountId, gson),
|
||||
status.reblog?.account?.toEntity(accountId, gson)
|
||||
)
|
||||
@ -361,7 +361,6 @@ fun Placeholder.toEntity(timelineUserId: Long): TimelineStatusEntity {
|
||||
}
|
||||
|
||||
fun Status.toEntity(timelineUserId: Long,
|
||||
htmlConverter: HtmlConverter,
|
||||
gson: Gson): TimelineStatusEntity {
|
||||
val actionable = actionableStatus
|
||||
return TimelineStatusEntity(
|
||||
@ -371,7 +370,7 @@ fun Status.toEntity(timelineUserId: Long,
|
||||
authorServerId = actionable.account.id,
|
||||
inReplyToId = actionable.inReplyToId,
|
||||
inReplyToAccountId = actionable.inReplyToAccountId,
|
||||
content = htmlConverter.toHtml(actionable.content),
|
||||
content = actionable.content.toHtml(),
|
||||
createdAt = actionable.createdAt.time,
|
||||
emojis = actionable.emojis.let(gson::toJson),
|
||||
reblogsCount = actionable.reblogsCount,
|
||||
@ -384,7 +383,7 @@ fun Status.toEntity(timelineUserId: Long,
|
||||
visibility = actionable.visibility,
|
||||
attachments = actionable.attachments.let(gson::toJson),
|
||||
mentions = actionable.mentions.let(gson::toJson),
|
||||
application = actionable.let(gson::toJson),
|
||||
application = actionable.application.let(gson::toJson),
|
||||
reblogServerId = reblog?.id,
|
||||
reblogAccountId = reblog?.let { this.account.id },
|
||||
poll = actionable.poll.let(gson::toJson)
|
||||
|
@ -1,22 +0,0 @@
|
||||
package com.keylesspalace.tusky.util
|
||||
|
||||
import android.text.Spanned
|
||||
|
||||
/**
|
||||
* Abstracting away Android-specific things.
|
||||
*/
|
||||
interface HtmlConverter {
|
||||
fun fromHtml(html: String): Spanned
|
||||
|
||||
fun toHtml(text: Spanned): String
|
||||
}
|
||||
|
||||
internal class HtmlConverterImpl : HtmlConverter {
|
||||
override fun fromHtml(html: String): Spanned {
|
||||
return HtmlUtils.fromHtml(html)
|
||||
}
|
||||
|
||||
override fun toHtml(text: Spanned): String {
|
||||
return HtmlUtils.toHtml(text)
|
||||
}
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
/* Copyright 2017 Andrew Dawson
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.util;
|
||||
|
||||
import android.os.Build;
|
||||
import android.text.Html;
|
||||
import android.text.Spanned;
|
||||
|
||||
public class HtmlUtils {
|
||||
private static CharSequence trimTrailingWhitespace(CharSequence s) {
|
||||
int i = s.length();
|
||||
do {
|
||||
i--;
|
||||
} while (i >= 0 && Character.isWhitespace(s.charAt(i)));
|
||||
return s.subSequence(0, i + 1);
|
||||
}
|
||||
|
||||
public static Spanned fromHtml(String html) {
|
||||
Spanned result;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
result = Html.fromHtml(html, Html.FROM_HTML_MODE_LEGACY);
|
||||
} else {
|
||||
result = Html.fromHtml(html);
|
||||
}
|
||||
/* Html.fromHtml returns trailing whitespace if the html ends in a </p> tag, which
|
||||
* all status contents do, so it should be trimmed. */
|
||||
return (Spanned) trimTrailingWhitespace(result);
|
||||
}
|
||||
|
||||
public static String toHtml(Spanned text) {
|
||||
String result;
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
result = Html.toHtml(text, Html.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE);
|
||||
} else {
|
||||
result = Html.toHtml(text);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
@ -172,15 +172,12 @@ class StatusViewHelper(private val itemView: View) {
|
||||
sensitiveMediaWarning.visibility = View.GONE
|
||||
sensitiveMediaShow.visibility = View.GONE
|
||||
} else {
|
||||
|
||||
val hiddenContentText: String = if (sensitive) {
|
||||
sensitiveMediaWarning.text = if (sensitive) {
|
||||
context.getString(R.string.status_sensitive_media_title)
|
||||
} else {
|
||||
context.getString(R.string.status_media_hidden_title)
|
||||
}
|
||||
|
||||
sensitiveMediaWarning.text = HtmlUtils.fromHtml(hiddenContentText)
|
||||
|
||||
sensitiveMediaWarning.visibility = if (showingContent) View.GONE else View.VISIBLE
|
||||
sensitiveMediaShow.visibility = if (showingContent) View.VISIBLE else View.GONE
|
||||
sensitiveMediaShow.setOnClickListener { v ->
|
||||
|
@ -18,10 +18,10 @@ package com.keylesspalace.tusky.viewdata
|
||||
import android.content.Context
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import androidx.core.text.parseAsHtml
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.entity.Poll
|
||||
import com.keylesspalace.tusky.entity.PollOption
|
||||
import com.keylesspalace.tusky.util.HtmlUtils
|
||||
import java.util.*
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@ -50,7 +50,7 @@ fun calculatePercent(fraction: Int, total: Int): Int {
|
||||
}
|
||||
|
||||
fun buildDescription(title: String, percent: Int, context: Context): Spanned {
|
||||
return SpannableStringBuilder(HtmlUtils.fromHtml(context.getString(R.string.poll_percent_format, percent)))
|
||||
return SpannableStringBuilder(context.getString(R.string.poll_percent_format, percent).parseAsHtml())
|
||||
.append(" ")
|
||||
.append(title)
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
package com.keylesspalace.tusky.fragment
|
||||
|
||||
import android.text.Spanned
|
||||
import android.text.SpannableString
|
||||
import android.text.SpannedString
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.google.gson.Gson
|
||||
import com.keylesspalace.tusky.SpanUtilsTest
|
||||
import com.keylesspalace.tusky.db.AccountEntity
|
||||
@ -12,7 +14,6 @@ import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.repository.*
|
||||
import com.keylesspalace.tusky.util.Either
|
||||
import com.keylesspalace.tusky.util.HtmlConverter
|
||||
import com.nhaarman.mockitokotlin2.isNull
|
||||
import com.nhaarman.mockitokotlin2.verify
|
||||
import com.nhaarman.mockitokotlin2.verifyNoMoreInteractions
|
||||
@ -24,14 +25,18 @@ import io.reactivex.schedulers.TestScheduler
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.mockito.ArgumentMatchers.any
|
||||
import org.mockito.ArgumentMatchers.anyInt
|
||||
import org.mockito.Mock
|
||||
import org.mockito.MockitoAnnotations
|
||||
import org.robolectric.annotation.Config
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.collections.ArrayList
|
||||
|
||||
@Config(sdk = [28])
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class TimelineRepositoryTest {
|
||||
@Mock
|
||||
lateinit var timelineDao: TimelineDao
|
||||
@ -56,15 +61,6 @@ class TimelineRepositoryTest {
|
||||
domain = "domain.com",
|
||||
isActive = true
|
||||
)
|
||||
private val htmlConverter = object : HtmlConverter {
|
||||
override fun fromHtml(html: String): Spanned {
|
||||
return SpanUtilsTest.FakeSpannable(html)
|
||||
}
|
||||
|
||||
override fun toHtml(text: Spanned): String {
|
||||
return text.toString()
|
||||
}
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
@ -74,8 +70,7 @@ class TimelineRepositoryTest {
|
||||
gson = Gson()
|
||||
testScheduler = TestScheduler()
|
||||
RxJavaPlugins.setIoSchedulerHandler { testScheduler }
|
||||
subject = TimelineRepositoryImpl(timelineDao, mastodonApi, accountManager, gson,
|
||||
htmlConverter)
|
||||
subject = TimelineRepositoryImpl(timelineDao, mastodonApi, accountManager, gson)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ -97,7 +92,7 @@ class TimelineRepositoryTest {
|
||||
verify(timelineDao).insertStatusIfNotThere(Placeholder("1").toEntity(account.id))
|
||||
for (status in statuses) {
|
||||
verify(timelineDao).insertInTransaction(
|
||||
status.toEntity(account.id, htmlConverter, gson),
|
||||
status.toEntity(account.id, gson),
|
||||
status.account.toEntity(account.id, gson),
|
||||
null
|
||||
)
|
||||
@ -129,7 +124,7 @@ class TimelineRepositoryTest {
|
||||
// We assume for now that overlapped one is inserted but it's not that important
|
||||
for (status in response) {
|
||||
verify(timelineDao).insertInTransaction(
|
||||
status.toEntity(account.id, htmlConverter, gson),
|
||||
status.toEntity(account.id, gson),
|
||||
status.account.toEntity(account.id, gson),
|
||||
null
|
||||
)
|
||||
@ -159,7 +154,7 @@ class TimelineRepositoryTest {
|
||||
verify(timelineDao).deleteRange(account.id, response.last().id, response.first().id)
|
||||
for (status in response) {
|
||||
verify(timelineDao).insertInTransaction(
|
||||
status.toEntity(account.id, htmlConverter, gson),
|
||||
status.toEntity(account.id, gson),
|
||||
status.account.toEntity(account.id, gson),
|
||||
null
|
||||
)
|
||||
@ -201,7 +196,7 @@ class TimelineRepositoryTest {
|
||||
// We assume for now that overlapped one is inserted but it's not that important
|
||||
for (status in response) {
|
||||
verify(timelineDao).insertInTransaction(
|
||||
status.toEntity(account.id, htmlConverter, gson),
|
||||
status.toEntity(account.id, gson),
|
||||
status.account.toEntity(account.id, gson),
|
||||
null
|
||||
)
|
||||
@ -246,7 +241,7 @@ class TimelineRepositoryTest {
|
||||
|
||||
for (status in response) {
|
||||
verify(timelineDao).insertInTransaction(
|
||||
status.toEntity(account.id, htmlConverter, gson),
|
||||
status.toEntity(account.id, gson),
|
||||
status.account.toEntity(account.id, gson),
|
||||
null
|
||||
)
|
||||
@ -263,7 +258,7 @@ class TimelineRepositoryTest {
|
||||
val status = makeStatus("2")
|
||||
val dbStatus = makeStatus("1")
|
||||
val dbResult = TimelineStatusWithAccount()
|
||||
dbResult.status = dbStatus.toEntity(account.id, htmlConverter, gson)
|
||||
dbResult.status = dbStatus.toEntity(account.id, gson)
|
||||
dbResult.account = status.account.toEntity(account.id, gson)
|
||||
|
||||
whenever(mastodonApi.homeTimelineSingle(any(), any(), any(), any()))
|
||||
@ -297,7 +292,7 @@ class TimelineRepositoryTest {
|
||||
return Status(
|
||||
id = id,
|
||||
account = account,
|
||||
content = SpanUtilsTest.FakeSpannable("hello$id"),
|
||||
content = SpannableString("hello$id"),
|
||||
createdAt = Date(),
|
||||
emojis = listOf(),
|
||||
reblogsCount = 3,
|
||||
@ -327,7 +322,7 @@ class TimelineRepositoryTest {
|
||||
localUsername = "test$id",
|
||||
username = "test$id@example.com",
|
||||
displayName = "Example Account $id",
|
||||
note = SpanUtilsTest.FakeSpannable("Note! $id"),
|
||||
note = SpannableString("Note! $id"),
|
||||
url = "https://example.com/@test$id",
|
||||
avatar = "avatar$id",
|
||||
header = "Header$id",
|
||||
|
Loading…
Reference in New Issue
Block a user