diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java index 746b1e84..3a885500 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java @@ -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 ""; } diff --git a/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt b/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt index d2e8dee1..b4650e35 100644 --- a/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt +++ b/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt @@ -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 diff --git a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt index 5cf4af5a..48845386 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/AppModule.kt @@ -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 diff --git a/app/src/main/java/com/keylesspalace/tusky/di/RepositoryModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/RepositoryModule.kt index 1b5206a7..af8cfb88 100644 --- a/app/src/main/java/com/keylesspalace/tusky/di/RepositoryModule.kt +++ b/app/src/main/java/com/keylesspalace/tusky/di/RepositoryModule.kt @@ -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) } } \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Account.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Account.kt index dbb17901..01d472b2 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Account.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Account.kt @@ -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() Spanned, + val note: Spanned, val url: String, val avatar: String, val header: String, @@ -46,7 +38,7 @@ data class Account( val fields: List? = 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? -): Parcelable +) -@Parcelize data class Field ( val name: String, - val value: @WriteWith() 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 { override fun create(parcel: Parcel): Spanned = HtmlUtils.fromHtml(parcel.readString()) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Card.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Card.kt index c6f82637..43e54f01 100644 --- a/app/src/main/java/com/keylesspalace/tusky/entity/Card.kt +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Card.kt @@ -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() Spanned, - val description: @WriteWith() 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() diff --git a/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt b/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt index e9bcfe2f..b8764445 100644 --- a/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt +++ b/app/src/main/java/com/keylesspalace/tusky/repository/TimelineRepository.kt @@ -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) diff --git a/app/src/main/java/com/keylesspalace/tusky/util/HtmlConverter.kt b/app/src/main/java/com/keylesspalace/tusky/util/HtmlConverter.kt deleted file mode 100644 index 72efc28c..00000000 --- a/app/src/main/java/com/keylesspalace/tusky/util/HtmlConverter.kt +++ /dev/null @@ -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) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/keylesspalace/tusky/util/HtmlUtils.java b/app/src/main/java/com/keylesspalace/tusky/util/HtmlUtils.java deleted file mode 100644 index cba8f2fa..00000000 --- a/app/src/main/java/com/keylesspalace/tusky/util/HtmlUtils.java +++ /dev/null @@ -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 . */ - -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

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; - } -} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt index 840e220e..7daec799 100644 --- a/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt +++ b/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt @@ -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 -> diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/PollViewData.kt b/app/src/main/java/com/keylesspalace/tusky/viewdata/PollViewData.kt index dff0c9f6..4cf16d0d 100644 --- a/app/src/main/java/com/keylesspalace/tusky/viewdata/PollViewData.kt +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/PollViewData.kt @@ -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) } diff --git a/app/src/test/java/com/keylesspalace/tusky/fragment/TimelineRepositoryTest.kt b/app/src/test/java/com/keylesspalace/tusky/fragment/TimelineRepositoryTest.kt index 74225017..5d982354 100644 --- a/app/src/test/java/com/keylesspalace/tusky/fragment/TimelineRepositoryTest.kt +++ b/app/src/test/java/com/keylesspalace/tusky/fragment/TimelineRepositoryTest.kt @@ -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",