diff --git a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt index 22144ad4..6b401f1c 100644 --- a/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt +++ b/app/src/main/java/com/futo/platformplayer/UISlideOverlays.kt @@ -32,7 +32,7 @@ import com.futo.platformplayer.models.Playlist import com.futo.platformplayer.models.Subscription import com.futo.platformplayer.parsers.HLS import com.futo.platformplayer.states.* -import com.futo.platformplayer.views.Loader +import com.futo.platformplayer.views.LoaderView import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuGroup import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuItem import com.futo.platformplayer.views.overlays.slideup.SlideUpMenuOverlay @@ -137,7 +137,7 @@ class UISlideOverlays { } fun showHlsPicker(video: IPlatformVideoDetails, source: Any, sourceUrl: String, container: ViewGroup): SlideUpMenuOverlay { - val items = arrayListOf(Loader(container.context)) + val items = arrayListOf(LoaderView(container.context)) val slideUpMenuOverlay = SlideUpMenuOverlay(container.context, container, container.context.getString(R.string.download_video), null, true, items) StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { @@ -501,7 +501,7 @@ class UISlideOverlays { val dp70 = 70.dp(container.context.resources); val dp15 = 15.dp(container.context.resources); val overlay = SlideUpMenuOverlay(container.context, container, text, null, true, listOf( - Loader(container.context, true, dp70).apply { + LoaderView(container.context, true, dp70).apply { this.setPadding(0, dp15, 0, dp15); } ), true); diff --git a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt index c1d849ef..a385170a 100644 --- a/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt +++ b/app/src/main/java/com/futo/platformplayer/activities/MainActivity.kt @@ -90,6 +90,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { lateinit var _fragMainPlaylistSearchResults: PlaylistSearchResultsFragment; lateinit var _fragMainSuggestions: SuggestionsFragment; lateinit var _fragMainSubscriptions: CreatorsFragment; + lateinit var _fragMainComments: CommentsFragment; lateinit var _fragMainSubscriptionsFeed: SubscriptionsFeedFragment; lateinit var _fragMainChannel: ChannelFragment; lateinit var _fragMainSources: SourcesFragment; @@ -205,6 +206,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { _fragMainCreatorSearchResults = CreatorSearchResultsFragment.newInstance(); _fragMainPlaylistSearchResults = PlaylistSearchResultsFragment.newInstance(); _fragMainSubscriptions = CreatorsFragment.newInstance(); + _fragMainComments = CommentsFragment.newInstance(); _fragMainChannel = ChannelFragment.newInstance(); _fragMainSubscriptionsFeed = SubscriptionsFeedFragment.newInstance(); _fragMainSources = SourcesFragment.newInstance(); @@ -282,6 +284,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { //Set top bars _fragMainHome.topBar = _fragTopBarGeneral; _fragMainSubscriptions.topBar = _fragTopBarGeneral; + _fragMainComments.topBar = _fragTopBarGeneral; _fragMainSuggestions.topBar = _fragTopBarSearch; _fragMainVideoSearchResults.topBar = _fragTopBarSearch; _fragMainCreatorSearchResults.topBar = _fragTopBarSearch; @@ -916,6 +919,7 @@ class MainActivity : AppCompatActivity, IWithResultLauncher { GeneralTopBarFragment::class -> _fragTopBarGeneral as T; SearchTopBarFragment::class -> _fragTopBarSearch as T; CreatorsFragment::class -> _fragMainSubscriptions as T; + CommentsFragment::class -> _fragMainComments as T; SubscriptionsFeedFragment::class -> _fragMainSubscriptionsFeed as T; PlaylistSearchResultsFragment::class -> _fragMainPlaylistSearchResults as T; ChannelFragment::class -> _fragMainChannel as T; diff --git a/app/src/main/java/com/futo/platformplayer/activities/SettingsActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/SettingsActivity.kt index 3e5259a9..8527e2d6 100644 --- a/app/src/main/java/com/futo/platformplayer/activities/SettingsActivity.kt +++ b/app/src/main/java/com/futo/platformplayer/activities/SettingsActivity.kt @@ -15,7 +15,7 @@ import androidx.lifecycle.lifecycleScope import com.futo.platformplayer.* import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.states.StateApp -import com.futo.platformplayer.views.Loader +import com.futo.platformplayer.views.LoaderView import com.futo.platformplayer.views.fields.FieldForm import com.futo.platformplayer.views.fields.ReadOnlyTextField import com.google.android.material.button.MaterialButton @@ -23,7 +23,7 @@ import com.google.android.material.button.MaterialButton class SettingsActivity : AppCompatActivity(), IWithResultLauncher { private lateinit var _form: FieldForm; private lateinit var _buttonBack: ImageButton; - private lateinit var _loader: Loader; + private lateinit var _loaderView: LoaderView; private lateinit var _devSets: LinearLayout; private lateinit var _buttonDev: MaterialButton; @@ -43,7 +43,7 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher { _buttonBack = findViewById(R.id.button_back); _buttonDev = findViewById(R.id.button_dev); _devSets = findViewById(R.id.dev_settings); - _loader = findViewById(R.id.loader); + _loaderView = findViewById(R.id.loader); _form.onChanged.subscribe { field, value -> Logger.i("SettingsActivity", "Setting [${field.field?.name}] changed, saving"); @@ -70,9 +70,9 @@ class SettingsActivity : AppCompatActivity(), IWithResultLauncher { fun reloadSettings() { _form.setSearchVisible(false); - _loader.start(); + _loaderView.start(); _form.fromObject(lifecycleScope, Settings.instance) { - _loader.stop(); + _loaderView.stop(); _form.setSearchVisible(true); var devCounter = 0; diff --git a/app/src/main/java/com/futo/platformplayer/api/media/models/comments/PolycentricPlatformComment.kt b/app/src/main/java/com/futo/platformplayer/api/media/models/comments/PolycentricPlatformComment.kt index 69c92f49..90a65e00 100644 --- a/app/src/main/java/com/futo/platformplayer/api/media/models/comments/PolycentricPlatformComment.kt +++ b/app/src/main/java/com/futo/platformplayer/api/media/models/comments/PolycentricPlatformComment.kt @@ -4,10 +4,7 @@ import com.futo.platformplayer.api.media.IPlatformClient import com.futo.platformplayer.api.media.models.PlatformAuthorLink import com.futo.platformplayer.api.media.models.ratings.IRating import com.futo.platformplayer.api.media.structures.IPager -import com.futo.platformplayer.polycentric.PolycentricCache -import com.futo.platformplayer.states.StatePolycentric import com.futo.polycentric.core.Pointer -import com.futo.polycentric.core.SignedEvent import userpackage.Protocol.Reference import java.time.OffsetDateTime @@ -20,16 +17,18 @@ class PolycentricPlatformComment : IPlatformComment { override val replyCount: Int?; + val eventPointer: Pointer; val reference: Reference; - constructor(contextUrl: String, author: PlatformAuthorLink, msg: String, rating: IRating, date: OffsetDateTime, reference: Reference, replyCount: Int? = null) { + constructor(contextUrl: String, author: PlatformAuthorLink, msg: String, rating: IRating, date: OffsetDateTime, eventPointer: Pointer, replyCount: Int? = null) { this.contextUrl = contextUrl; this.author = author; this.message = msg; this.rating = rating; this.date = date; this.replyCount = replyCount; - this.reference = reference; + this.eventPointer = eventPointer; + this.reference = eventPointer.toReference(); } override fun getReplies(client: IPlatformClient): IPager { @@ -37,7 +36,7 @@ class PolycentricPlatformComment : IPlatformComment { } fun cloneWithUpdatedReplyCount(replyCount: Int?): PolycentricPlatformComment { - return PolycentricPlatformComment(contextUrl, author, message, rating, date, reference, replyCount); + return PolycentricPlatformComment(contextUrl, author, message, rating, date, eventPointer, replyCount); } companion object { diff --git a/app/src/main/java/com/futo/platformplayer/dialogs/CommentDialog.kt b/app/src/main/java/com/futo/platformplayer/dialogs/CommentDialog.kt index 584c8465..cc9015eb 100644 --- a/app/src/main/java/com/futo/platformplayer/dialogs/CommentDialog.kt +++ b/app/src/main/java/com/futo/platformplayer/dialogs/CommentDialog.kt @@ -118,7 +118,7 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol msg = comment, rating = RatingLikeDislikes(0, 0), date = OffsetDateTime.now(), - reference = eventPointer.toReference() + eventPointer = eventPointer )); dismiss(); diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt index 6d7b8991..f2e420b9 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/bottombar/MenuBottomBarFragment.kt @@ -351,6 +351,7 @@ class MenuBottomBarFragment : MainActivityFragment() { ButtonDefinition(4, R.drawable.ic_playlist, R.drawable.ic_playlist_filled, R.string.playlists, canToggle = false, { it.currentMain is PlaylistsFragment }, { it.navigate() }), ButtonDefinition(5, R.drawable.ic_history, R.drawable.ic_history, R.string.history, canToggle = false, { it.currentMain is HistoryFragment }, { it.navigate() }), ButtonDefinition(6, R.drawable.ic_download, R.drawable.ic_download, R.string.downloads, canToggle = false, { it.currentMain is DownloadsFragment }, { it.navigate() }), + ButtonDefinition(8, R.drawable.ic_chat, R.drawable.ic_chat_filled, R.string.comments, canToggle = true, { it.currentMain is CommentsFragment }, { it.navigate() }), ButtonDefinition(7, R.drawable.ic_settings, R.drawable.ic_settings, R.string.settings, canToggle = false, { false }, { val c = it.context ?: return@ButtonDefinition; Logger.i(TAG, "settings preventPictureInPicture()"); diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/BuyFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/BuyFragment.kt index e0d13b28..ca970cfb 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/BuyFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/BuyFragment.kt @@ -35,6 +35,11 @@ class BuyFragment : MainFragment() { return view; } + override fun onDestroyMainView() { + super.onDestroyMainView() + _view = null + } + class BuyView: LinearLayout { private val _fragment: BuyFragment; diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/CommentsFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/CommentsFragment.kt new file mode 100644 index 00000000..97d5200e --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/CommentsFragment.kt @@ -0,0 +1,306 @@ +package com.futo.platformplayer.fragment.mainactivity.main + +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.ViewPropertyAnimator +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.FrameLayout +import android.widget.LinearLayout +import android.widget.Spinner +import android.widget.TextView +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.futo.platformplayer.R +import com.futo.platformplayer.UIDialogs +import com.futo.platformplayer.activities.PolycentricHomeActivity +import com.futo.platformplayer.api.media.models.comments.IPlatformComment +import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment +import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails +import com.futo.platformplayer.constructs.TaskHandler +import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions +import com.futo.platformplayer.logging.Logger +import com.futo.platformplayer.states.StateApp +import com.futo.platformplayer.states.StatePlatform +import com.futo.platformplayer.states.StatePolycentric +import com.futo.platformplayer.views.adapters.CommentWithReferenceViewHolder +import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader +import com.futo.platformplayer.views.overlays.RepliesOverlay +import com.futo.polycentric.core.PublicKey +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.net.UnknownHostException + +class CommentsFragment : MainFragment() { + override val isMainView : Boolean = true + override val isTab: Boolean = true + override val hasBottomBar: Boolean get() = true + + private var _view: CommentsView? = null + + override fun onShownWithView(parameter: Any?, isBack: Boolean) { + super.onShownWithView(parameter, isBack) + _view?.onShown() + } + + override fun onCreateMainView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + val view = CommentsView(this, inflater) + _view = view + return view + } + + override fun onDestroyMainView() { + super.onDestroyMainView() + _view = null + } + + override fun onBackPressed(): Boolean { + return _view?.onBackPressed() ?: false + } + + override fun onResume() { + super.onResume() + _view?.onShown() + } + + companion object { + fun newInstance() = CommentsFragment().apply {} + private const val TAG = "CommentsFragment" + } + + class CommentsView : FrameLayout { + private val _fragment: CommentsFragment + private val _recyclerComments: RecyclerView; + private val _adapterComments: InsertedViewAdapterWithLoader; + private val _textCommentCount: TextView + private val _comments: ArrayList = arrayListOf(); + private val _llmReplies: LinearLayoutManager; + private val _spinnerSortBy: Spinner; + private val _layoutNotLoggedIn: LinearLayout; + private val _buttonLogin: LinearLayout; + private var _loading = false; + private val _repliesOverlay: RepliesOverlay; + private var _repliesAnimator: ViewPropertyAnimator? = null; + + private val _taskLoadComments = if(!isInEditMode) TaskHandler>( + StateApp.instance.scopeGetter, { StatePolycentric.instance.getSystemComments(context, it) }) + .success { pager -> onCommentsLoaded(pager); } + .exception { + UIDialogs.toast("Failed to load comments"); + setLoading(false); + } + .exception { + Logger.e(TAG, "Failed to load comments.", it); + UIDialogs.toast(context, context.getString(R.string.failed_to_load_comments) + "\n" + (it.message ?: "")); + setLoading(false); + } else TaskHandler(IPlatformVideoDetails::class.java, StateApp.instance.scopeGetter); + + constructor(fragment: CommentsFragment, inflater: LayoutInflater) : super(inflater.context) { + _fragment = fragment + inflater.inflate(R.layout.fragment_comments, this) + + val commentHeader = findViewById(R.id.layout_header) + (commentHeader.parent as ViewGroup).removeView(commentHeader) + _textCommentCount = commentHeader.findViewById(R.id.text_comment_count) + + _recyclerComments = findViewById(R.id.recycler_comments) + _adapterComments = InsertedViewAdapterWithLoader(context, arrayListOf(commentHeader), arrayListOf(), + childCountGetter = { _comments.size }, + childViewHolderBinder = { viewHolder, position -> viewHolder.bind(_comments[position]); }, + childViewHolderFactory = { viewGroup, _ -> + val holder = CommentWithReferenceViewHolder(viewGroup); + holder.onDelete.subscribe(::onDelete); + holder.onRepliesClick.subscribe(::onRepliesClick); + return@InsertedViewAdapterWithLoader holder; + } + ); + + _spinnerSortBy = commentHeader.findViewById(R.id.spinner_sortby); + _spinnerSortBy.adapter = ArrayAdapter(context, R.layout.spinner_item_simple, resources.getStringArray(R.array.comments_sortby_array)).also { + it.setDropDownViewResource(R.layout.spinner_dropdownitem_simple); + }; + _spinnerSortBy.setSelection(0); + _spinnerSortBy.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected(parent: AdapterView<*>, view: View?, pos: Int, id: Long) { + if (_spinnerSortBy.selectedItemPosition == 0) { + _comments.sortByDescending { it.date!! } + } else if (_spinnerSortBy.selectedItemPosition == 1) { + _comments.sortBy { it.date!! } + } + + _adapterComments.notifyDataSetChanged() + } + override fun onNothingSelected(parent: AdapterView<*>?) = Unit + } + + _llmReplies = LinearLayoutManager(context); + _recyclerComments.layoutManager = _llmReplies; + _recyclerComments.adapter = _adapterComments; + updateCommentCountString(); + + _layoutNotLoggedIn = findViewById(R.id.layout_not_logged_in) + _layoutNotLoggedIn.visibility = View.GONE + + _buttonLogin = findViewById(R.id.button_login) + _buttonLogin.setOnClickListener { + context.startActivity(Intent(context, PolycentricHomeActivity::class.java)); + } + + _repliesOverlay = findViewById(R.id.replies_overlay); + _repliesOverlay.onClose.subscribe { setRepliesOverlayVisible(isVisible = false, animate = true); }; + } + + private fun onDelete(comment: IPlatformComment) { + val processHandle = StatePolycentric.instance.processHandle ?: return + if (comment !is PolycentricPlatformComment) { + return + } + + val index = _comments.indexOf(comment) + if (index != -1) { + _comments.removeAt(index) + _adapterComments.notifyItemRemoved(_adapterComments.childToParentPosition(index)) + + StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { + try { + processHandle.delete(comment.eventPointer.process, comment.eventPointer.logicalClock) + } catch (e: Throwable) { + Logger.e(TAG, "Failed to delete event.", e); + return@launch; + } + + try { + Logger.i(TAG, "Started backfill"); + processHandle.fullyBackfillServersAnnounceExceptions(); + Logger.i(TAG, "Finished backfill"); + } catch (e: Throwable) { + Logger.e(TAG, "Failed to fully backfill servers.", e); + } + } + } + } + + fun onBackPressed(): Boolean { + if (_repliesOverlay.visibility == View.VISIBLE) { + setRepliesOverlayVisible(isVisible = false, animate = true); + return true + } + + return false + } + + private fun onRepliesClick(c: IPlatformComment) { + val replyCount = c.replyCount ?: 0; + var metadata = ""; + if (replyCount > 0) { + metadata += "$replyCount " + context.getString(R.string.replies); + } + + if (c is PolycentricPlatformComment) { + var parentComment: PolycentricPlatformComment = c; + _repliesOverlay.load(false, metadata, c.contextUrl, c.reference, c, + { StatePolycentric.instance.getCommentPager(c.contextUrl, c.reference) }, + { + val newComment = parentComment.cloneWithUpdatedReplyCount((parentComment.replyCount ?: 0) + 1); + val index = _comments.indexOf(c); + _comments[index] = newComment; + _adapterComments.notifyItemChanged(_adapterComments.childToParentPosition(index)); + parentComment = newComment; + }); + } else { + _repliesOverlay.load(true, metadata, null, null, c, { StatePlatform.instance.getSubComments(c) }); + } + + setRepliesOverlayVisible(isVisible = true, animate = true); + } + + private fun setRepliesOverlayVisible(isVisible: Boolean, animate: Boolean) { + val desiredVisibility = if (isVisible) View.VISIBLE else View.GONE + if (_repliesOverlay.visibility == desiredVisibility) { + return; + } + + _repliesAnimator?.cancel(); + + if (isVisible) { + _repliesOverlay.visibility = View.VISIBLE; + + if (animate) { + _repliesOverlay.translationY = _repliesOverlay.height.toFloat(); + + _repliesAnimator = _repliesOverlay.animate() + .setDuration(300) + .translationY(0f) + .withEndAction { + _repliesAnimator = null; + }.apply { start() }; + } + } else { + if (animate) { + _repliesOverlay.translationY = 0f; + + _repliesAnimator = _repliesOverlay.animate() + .setDuration(300) + .translationY(_repliesOverlay.height.toFloat()) + .withEndAction { + _repliesOverlay.visibility = GONE; + _repliesAnimator = null; + }.apply { start(); } + } else { + _repliesOverlay.visibility = View.GONE; + _repliesOverlay.translationY = _repliesOverlay.height.toFloat(); + } + } + } + + private fun updateCommentCountString() { + _textCommentCount.text = context.getString(R.string.these_are_all_commentcount_comments_you_have_made_in_grayjay).replace("{commentCount}", _comments.size.toString()) + } + + private fun setLoading(loading: Boolean) { + if (_loading == loading) { + return; + } + + _loading = loading; + _adapterComments.setLoading(loading); + } + + private fun fetchComments() { + val system = StatePolycentric.instance.processHandle?.system ?: return + _comments.clear() + _adapterComments.notifyDataSetChanged() + setLoading(true) + _taskLoadComments.run(system) + } + + private fun onCommentsLoaded(comments: List) { + setLoading(false) + _comments.addAll(comments) + + if (_spinnerSortBy.selectedItemPosition == 0) { + _comments.sortByDescending { it.date!! } + } else if (_spinnerSortBy.selectedItemPosition == 1) { + _comments.sortBy { it.date!! } + } + + _adapterComments.notifyDataSetChanged() + updateCommentCountString() + } + + fun onShown() { + val processHandle = StatePolycentric.instance.processHandle + if (processHandle != null) { + _layoutNotLoggedIn.visibility = View.GONE + _recyclerComments.visibility = View.VISIBLE + fetchComments() + } else { + _layoutNotLoggedIn.visibility = View.VISIBLE + _recyclerComments.visibility= View.GONE + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PostDetailFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PostDetailFragment.kt index b35cd912..667b6ac9 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PostDetailFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PostDetailFragment.kt @@ -224,7 +224,7 @@ class PostDetailFragment : MainFragment { updateCommentType(false); }; - _commentsList.onClick.subscribe { c -> + _commentsList.onRepliesClick.subscribe { c -> val replyCount = c.replyCount ?: 0; var metadata = ""; if (replyCount > 0) { @@ -233,7 +233,7 @@ class PostDetailFragment : MainFragment { if (c is PolycentricPlatformComment) { var parentComment: PolycentricPlatformComment = c; - _repliesOverlay.load(_toggleCommentType.value, metadata, c.contextUrl, c.reference, + _repliesOverlay.load(_toggleCommentType.value, metadata, c.contextUrl, c.reference, c, { StatePolycentric.instance.getCommentPager(c.contextUrl, c.reference) }, { val newComment = parentComment.cloneWithUpdatedReplyCount((parentComment.replyCount ?: 0) + 1); @@ -241,7 +241,7 @@ class PostDetailFragment : MainFragment { parentComment = newComment; }); } else { - _repliesOverlay.load(_toggleCommentType.value, metadata, null, null, { StatePlatform.instance.getSubComments(c) }); + _repliesOverlay.load(_toggleCommentType.value, metadata, null, null, c, { StatePlatform.instance.getSubComments(c) }); } setRepliesOverlayVisible(isVisible = true, animate = true); diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt index 6a84494a..c65cffa2 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoDetailView.kt @@ -37,7 +37,6 @@ import com.futo.platformplayer.api.media.LiveChatManager import com.futo.platformplayer.api.media.PlatformID import com.futo.platformplayer.api.media.exceptions.ContentNotAvailableYetException import com.futo.platformplayer.api.media.exceptions.NoPlatformClientException -import com.futo.platformplayer.api.media.models.PlatformAuthorLink import com.futo.platformplayer.api.media.models.PlatformAuthorMembershipLink import com.futo.platformplayer.api.media.models.chapters.ChapterType import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment @@ -52,7 +51,6 @@ import com.futo.platformplayer.api.media.models.subtitles.ISubtitleSource import com.futo.platformplayer.api.media.models.video.IPlatformVideo import com.futo.platformplayer.api.media.models.video.IPlatformVideoDetails import com.futo.platformplayer.api.media.models.video.SerializedPlatformVideo -import com.futo.platformplayer.api.media.platforms.js.models.IJSContentDetails import com.futo.platformplayer.api.media.platforms.js.models.JSVideoDetails import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.casting.CastConnectionState @@ -60,7 +58,6 @@ import com.futo.platformplayer.casting.StateCasting import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.TaskHandler -import com.futo.platformplayer.dialogs.AutoUpdateDialog import com.futo.platformplayer.downloads.VideoLocal import com.futo.platformplayer.engine.exceptions.ScriptAgeException import com.futo.platformplayer.engine.exceptions.ScriptException @@ -109,7 +106,6 @@ import java.time.OffsetDateTime import kotlin.collections.ArrayList import kotlin.math.abs import kotlin.math.roundToLong -import kotlin.streams.toList class VideoDetailView : ConstraintLayout { @@ -578,7 +574,7 @@ class VideoDetailView : ConstraintLayout { _container_content_current = _container_content_main; - _commentsList.onClick.subscribe { c -> + _commentsList.onRepliesClick.subscribe { c -> val replyCount = c.replyCount ?: 0; var metadata = ""; if (replyCount > 0) { @@ -587,7 +583,7 @@ class VideoDetailView : ConstraintLayout { if (c is PolycentricPlatformComment) { var parentComment: PolycentricPlatformComment = c; - _container_content_replies.load(_toggleCommentType.value, metadata, c.contextUrl, c.reference, + _container_content_replies.load(_toggleCommentType.value, metadata, c.contextUrl, c.reference, c, { StatePolycentric.instance.getCommentPager(c.contextUrl, c.reference) }, { val newComment = parentComment.cloneWithUpdatedReplyCount((parentComment.replyCount ?: 0) + 1); @@ -595,7 +591,7 @@ class VideoDetailView : ConstraintLayout { parentComment = newComment; }); } else { - _container_content_replies.load(_toggleCommentType.value, metadata, null, null, { StatePlatform.instance.getSubComments(c) }); + _container_content_replies.load(_toggleCommentType.value, metadata, null, null, c, { StatePlatform.instance.getSubComments(c) }); } switchContentView(_container_content_replies); }; diff --git a/app/src/main/java/com/futo/platformplayer/states/StatePolycentric.kt b/app/src/main/java/com/futo/platformplayer/states/StatePolycentric.kt index f294a6f9..af91da21 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StatePolycentric.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StatePolycentric.kt @@ -218,6 +218,59 @@ class StatePolycentric { } } + fun getSystemComments(context: Context, system: PublicKey): List { + val dp_25 = 25.dp(context.resources) + val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(system)) + val author = system.systemToURLInfoSystemLinkUrl(systemState.servers.asIterable()) + val posts = arrayListOf() + Store.instance.enumerateSignedEvents(system, ContentType.POST) { se -> + val ev = se.event + val post = Protocol.Post.parseFrom(ev.content) + + posts.add(PolycentricPlatformComment( + contextUrl = author, + author = PlatformAuthorLink( + id = PlatformID("polycentric", author, null, ClaimType.POLYCENTRIC.value.toInt()), + name = systemState.username, + url = author, + thumbnail = systemState.avatar?.selectBestImage(dp_25 * dp_25)?.let { img -> img.toURLInfoSystemLinkUrl(system.toProto(), img.process, listOf(PolycentricCache.SERVER)) }, + subscribers = null + ), + msg = if (post.content.count() > PolycentricPlatformComment.MAX_COMMENT_SIZE) post.content.substring(0, PolycentricPlatformComment.MAX_COMMENT_SIZE) else post.content, + rating = RatingLikeDislikes(0, 0), + date = if (ev.unixMilliseconds != null) Instant.ofEpochMilli(ev.unixMilliseconds!!).atOffset(ZoneOffset.UTC) else OffsetDateTime.MIN, + replyCount = 0, + eventPointer = se.toPointer() + )) + } + + return posts + } + + suspend fun getLiveComment(contextUrl: String, reference: Protocol.Reference): PolycentricPlatformComment { + val response = ApiMethods.getQueryReferences(PolycentricCache.SERVER, reference, null, + Protocol.QueryReferencesRequestEvents.newBuilder() + .setFromType(ContentType.POST.value) + .addAllCountLwwElementReferences(arrayListOf( + Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder() + .setFromType(ContentType.OPINION.value) + .setValue(ByteString.copyFrom(Opinion.like.data)) + .build(), + Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder() + .setFromType(ContentType.OPINION.value) + .setValue(ByteString.copyFrom(Opinion.dislike.data)) + .build() + )) + .addCountReferences( + Protocol.QueryReferencesRequestCountReferences.newBuilder() + .setFromType(ContentType.POST.value) + .build()) + .build() + ) + + return mapQueryReferences(contextUrl, response).first() + } + suspend fun getCommentPager(contextUrl: String, reference: Protocol.Reference): IPager { val response = ApiMethods.getQueryReferences(PolycentricCache.SERVER, reference, null, Protocol.QueryReferencesRequestEvents.newBuilder() @@ -284,7 +337,7 @@ class StatePolycentric { }; } - private suspend fun mapQueryReferences(contextUrl: String, response: Protocol.QueryReferencesResponse): List { + private suspend fun mapQueryReferences(contextUrl: String, response: Protocol.QueryReferencesResponse): List { return response.itemsList.mapNotNull { val sev = SignedEvent.fromProto(it.event); val ev = sev.event; @@ -338,7 +391,7 @@ class StatePolycentric { rating = RatingLikeDislikes(likes, dislikes), date = if (unixMilliseconds != null) Instant.ofEpochMilli(unixMilliseconds).atOffset(ZoneOffset.UTC) else OffsetDateTime.MIN, replyCount = replies.toInt(), - reference = sev.toPointer().toReference() + eventPointer = sev.toPointer() ); } catch (e: Throwable) { return@mapNotNull null; diff --git a/app/src/main/java/com/futo/platformplayer/views/Loader.kt b/app/src/main/java/com/futo/platformplayer/views/LoaderView.kt similarity index 83% rename from app/src/main/java/com/futo/platformplayer/views/Loader.kt rename to app/src/main/java/com/futo/platformplayer/views/LoaderView.kt index 8e4a64d3..2e0610e3 100644 --- a/app/src/main/java/com/futo/platformplayer/views/Loader.kt +++ b/app/src/main/java/com/futo/platformplayer/views/LoaderView.kt @@ -1,6 +1,7 @@ package com.futo.platformplayer.views import android.content.Context +import android.graphics.Color import android.graphics.drawable.Animatable import android.util.AttributeSet import android.view.LayoutInflater @@ -11,9 +12,10 @@ import android.widget.LinearLayout import androidx.core.view.updateLayoutParams import com.futo.platformplayer.R -class Loader : LinearLayout { +class LoaderView : LinearLayout { private val _imageLoader: ImageView; private val _automatic: Boolean; + private var _isWhite: Boolean; private val _animatable: Animatable; constructor(context: Context, attrs: AttributeSet? = null) : super(context, attrs) { @@ -24,18 +26,25 @@ class Loader : LinearLayout { if (attrs != null) { val attrArr = context.obtainStyledAttributes(attrs, R.styleable.LoaderView, 0, 0); _automatic = attrArr.getBoolean(R.styleable.LoaderView_automatic, false); + _isWhite = attrArr.getBoolean(R.styleable.LoaderView_isWhite, false); attrArr.recycle(); } else { _automatic = false; + _isWhite = false; } visibility = View.GONE; + + if (_isWhite) { + _imageLoader.setColorFilter(Color.WHITE) + } } - constructor(context: Context, automatic: Boolean, height: Int = -1) : super(context) { + constructor(context: Context, automatic: Boolean, height: Int = -1, isWhite: Boolean = false) : super(context) { inflate(context, R.layout.view_loader, this); _imageLoader = findViewById(R.id.image_loader); _animatable = _imageLoader.drawable as Animatable; _automatic = automatic; + _isWhite = isWhite; if(height > 0) { layoutParams = ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, height); diff --git a/app/src/main/java/com/futo/platformplayer/views/MonetizationView.kt b/app/src/main/java/com/futo/platformplayer/views/MonetizationView.kt index 4a6f4677..d9e1011c 100644 --- a/app/src/main/java/com/futo/platformplayer/views/MonetizationView.kt +++ b/app/src/main/java/com/futo/platformplayer/views/MonetizationView.kt @@ -41,7 +41,7 @@ class MonetizationView : LinearLayout { private val _textMerchandise: TextView; private val _recyclerMerchandise: RecyclerView; - private val _loaderMerchandise: Loader; + private val _loaderViewMerchandise: LoaderView; private val _layoutMerchandise: FrameLayout; private var _merchandiseAdapterView: AnyAdapterView? = null; @@ -81,7 +81,7 @@ class MonetizationView : LinearLayout { _textMerchandise = findViewById(R.id.text_merchandise); _recyclerMerchandise = findViewById(R.id.recycler_merchandise); - _loaderMerchandise = findViewById(R.id.loader_merchandise); + _loaderViewMerchandise = findViewById(R.id.loader_merchandise); _layoutMerchandise = findViewById(R.id.layout_merchandise); _root = findViewById(R.id.root); @@ -108,7 +108,7 @@ class MonetizationView : LinearLayout { } private fun setMerchandise(items: List?) { - _loaderMerchandise.stop(); + _loaderViewMerchandise.stop(); if (items == null) { _textMerchandise.visibility = View.GONE; @@ -147,7 +147,7 @@ class MonetizationView : LinearLayout { val uri = Uri.parse(storeData); if (uri.isAbsolute) { _taskLoadMerchandise.run(storeData); - _loaderMerchandise.start(); + _loaderViewMerchandise.start(); } else { Logger.i(TAG, "Merchandise not loaded, not URL nor JSON") } diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/CommentViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/CommentViewHolder.kt index 79660207..89c3c1bb 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/CommentViewHolder.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/CommentViewHolder.kt @@ -3,6 +3,7 @@ package com.futo.platformplayer.views.adapters import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.FrameLayout import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView @@ -37,8 +38,10 @@ class CommentViewHolder : ViewHolder { private val _layoutRating: LinearLayout; private val _pillRatingLikesDislikes: PillRatingLikesDislikes; private val _layoutComment: ConstraintLayout; + private val _buttonDelete: FrameLayout; - var onClick = Event1(); + var onRepliesClick = Event1(); + var onDelete = Event1(); var comment: IPlatformComment? = null private set; @@ -55,6 +58,7 @@ class CommentViewHolder : ViewHolder { _buttonReplies = itemView.findViewById(R.id.button_replies); _layoutRating = itemView.findViewById(R.id.layout_rating); _pillRatingLikesDislikes = itemView.findViewById(R.id.rating); + _buttonDelete = itemView.findViewById(R.id.button_delete); _pillRatingLikesDislikes.onLikeDislikeUpdated.subscribe { args -> val c = comment @@ -87,7 +91,12 @@ class CommentViewHolder : ViewHolder { _buttonReplies.onClick.subscribe { val c = comment ?: return@subscribe; - onClick.emit(c); + onRepliesClick.emit(c); + } + + _buttonDelete.setOnClickListener { + val c = comment ?: return@setOnClickListener; + onDelete.emit(c); } _textBody.setPlatformPlayerLinkMovementMethod(viewGroup.context); @@ -167,6 +176,13 @@ class CommentViewHolder : ViewHolder { _buttonReplies.visibility = View.GONE; } + val processHandle = StatePolycentric.instance.processHandle + if (processHandle != null && comment is PolycentricPlatformComment && processHandle.system == comment.eventPointer.system) { + _buttonDelete.visibility = View.VISIBLE + } else { + _buttonDelete.visibility = View.GONE + } + this.comment = comment; } diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/CommentWithReferenceViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/CommentWithReferenceViewHolder.kt new file mode 100644 index 00000000..45b1be78 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/CommentWithReferenceViewHolder.kt @@ -0,0 +1,165 @@ +package com.futo.platformplayer.views.adapters + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.recyclerview.widget.RecyclerView.ViewHolder +import com.futo.platformplayer.* +import com.futo.platformplayer.api.media.models.comments.IPlatformComment +import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment +import com.futo.platformplayer.api.media.models.ratings.RatingLikeDislikes +import com.futo.platformplayer.constructs.Event1 +import com.futo.platformplayer.constructs.TaskHandler +import com.futo.platformplayer.logging.Logger +import com.futo.platformplayer.states.StateApp +import com.futo.platformplayer.states.StatePolycentric +import com.futo.platformplayer.views.others.CreatorThumbnail +import com.futo.platformplayer.views.pills.PillButton +import com.futo.platformplayer.views.pills.PillRatingLikesDislikes +import com.futo.polycentric.core.Opinion +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class CommentWithReferenceViewHolder : ViewHolder { + private val _creatorThumbnail: CreatorThumbnail; + private val _textAuthor: TextView; + private val _textMetadata: TextView; + private val _textBody: TextView; + private val _buttonReplies: PillButton; + private val _pillRatingLikesDislikes: PillRatingLikesDislikes; + private val _layoutComment: ConstraintLayout; + private val _buttonDelete: FrameLayout; + + private val _taskGetLiveComment = TaskHandler(StateApp.instance.scopeGetter, { StatePolycentric.instance.getLiveComment(it.contextUrl, it.reference) }) + .success { + bind(it, true); + } + .exception { + Logger.w(TAG, "Failed to get live comment.", it); + //TODO: Show error + } + + var onRepliesClick = Event1(); + var onDelete = Event1(); + var comment: IPlatformComment? = null + private set; + + constructor(viewGroup: ViewGroup) : super(LayoutInflater.from(viewGroup.context).inflate(R.layout.list_comment_with_reference, viewGroup, false)) { + _layoutComment = itemView.findViewById(R.id.layout_comment); + _creatorThumbnail = itemView.findViewById(R.id.image_thumbnail); + _textAuthor = itemView.findViewById(R.id.text_author); + _textMetadata = itemView.findViewById(R.id.text_metadata); + _textBody = itemView.findViewById(R.id.text_body); + _buttonReplies = itemView.findViewById(R.id.button_replies); + _pillRatingLikesDislikes = itemView.findViewById(R.id.rating); + _buttonDelete = itemView.findViewById(R.id.button_delete) + + _pillRatingLikesDislikes.onLikeDislikeUpdated.subscribe { args -> + val c = comment + if (c !is PolycentricPlatformComment) { + throw Exception("Not implemented for non polycentric comments") + } + + if (args.hasLiked) { + args.processHandle.opinion(c.reference, Opinion.like); + } else if (args.hasDisliked) { + args.processHandle.opinion(c.reference, Opinion.dislike); + } else { + args.processHandle.opinion(c.reference, Opinion.neutral); + } + + _layoutComment.alpha = if (args.dislikes > 2 && args.dislikes.toFloat() / (args.likes + args.dislikes).toFloat() >= 0.7f) 0.5f else 1.0f; + + StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { + try { + Logger.i(TAG, "Started backfill"); + args.processHandle.fullyBackfillServersAnnounceExceptions(); + Logger.i(TAG, "Finished backfill"); + } catch (e: Throwable) { + Logger.e(TAG, "Failed to backfill servers.", e) + } + } + + StatePolycentric.instance.updateLikeMap(c.reference, args.hasLiked, args.hasDisliked) + }; + + _buttonReplies.onClick.subscribe { + val c = comment ?: return@subscribe; + onRepliesClick.emit(c); + } + + _buttonDelete.setOnClickListener { + val c = comment ?: return@setOnClickListener; + onDelete.emit(c); + } + + _textBody.setPlatformPlayerLinkMovementMethod(viewGroup.context); + } + + fun bind(comment: IPlatformComment, live: Boolean = false) { + _taskGetLiveComment.cancel() + + _creatorThumbnail.setThumbnail(comment.author.thumbnail, false); + _creatorThumbnail.setHarborAvailable(comment is PolycentricPlatformComment,false); + _textAuthor.text = comment.author.name; + + val date = comment.date; + if (date != null) { + _textMetadata.visibility = View.VISIBLE; + _textMetadata.text = " • ${date.toHumanNowDiffString()} ago"; + } else { + _textMetadata.visibility = View.GONE; + } + + val rating = comment.rating; + if (rating is RatingLikeDislikes) { + _layoutComment.alpha = if (rating.dislikes > 2 && rating.dislikes.toFloat() / (rating.likes + rating.dislikes).toFloat() >= 0.7f) 0.5f else 1.0f; + } else { + _layoutComment.alpha = 1.0f; + } + + _textBody.text = comment.message.fixHtmlLinks(); + + if (comment is PolycentricPlatformComment) { + if (live) { + val hasLiked = StatePolycentric.instance.hasLiked(comment.reference); + val hasDisliked = StatePolycentric.instance.hasDisliked(comment.reference); + _pillRatingLikesDislikes.setRating(comment.rating, hasLiked, hasDisliked); + } else { + _pillRatingLikesDislikes.setLoading(true) + } + + if (live) { + _buttonReplies.setLoading(false) + + val replies = comment.replyCount ?: 0; + if (replies > 0) { + _buttonReplies.visibility = View.VISIBLE; + _buttonReplies.text.text = "$replies " + itemView.context.getString(R.string.replies); + } else { + _buttonReplies.visibility = View.GONE; + } + } else { + _buttonReplies.setLoading(true) + } + + if (false) { + //Restore from cached + } else { + //_taskGetLiveComment.run(comment) + } + } else { + _pillRatingLikesDislikes.visibility = View.GONE + _buttonReplies.visibility = View.GONE + } + + this.comment = comment; + } + + companion object { + private const val TAG = "CommentWithReferenceViewHolder"; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/SubscriptionAdapter.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/SubscriptionAdapter.kt index 75e57c47..f7a63d53 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/SubscriptionAdapter.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/SubscriptionAdapter.kt @@ -17,8 +17,8 @@ class SubscriptionAdapter : RecyclerView.Adapter { var onSettings = Event1(); var sortBy: Int = 3 set(value) { - field = value; - updateDataset(); + field = value + updateDataset() } constructor(inflater: LayoutInflater, confirmationMessage: String) : super() { diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewNestedVideoView.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewNestedVideoView.kt index 6644d7eb..d66bb466 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewNestedVideoView.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewNestedVideoView.kt @@ -19,7 +19,7 @@ import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.video.PlayerManager import com.futo.platformplayer.views.FeedStyle -import com.futo.platformplayer.views.Loader +import com.futo.platformplayer.views.LoaderView import com.futo.platformplayer.views.platform.PlatformIndicator import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -28,7 +28,7 @@ class PreviewNestedVideoView : PreviewVideoView { protected val _platformIndicatorNested: PlatformIndicator; protected val _containerLoader: LinearLayout; - protected val _loader: Loader; + protected val _loaderView: LoaderView; protected val _containerUnavailable: LinearLayout; protected val _textNestedUrl: TextView; @@ -42,7 +42,7 @@ class PreviewNestedVideoView : PreviewVideoView { constructor(context: Context, feedStyle: FeedStyle, exoPlayer: PlayerManager? = null): super(context, feedStyle, exoPlayer) { _platformIndicatorNested = findViewById(R.id.thumbnail_platform_nested); _containerLoader = findViewById(R.id.container_loader); - _loader = findViewById(R.id.loader); + _loaderView = findViewById(R.id.loader); _containerUnavailable = findViewById(R.id.container_unavailable); _textNestedUrl = findViewById(R.id.text_nested_url); @@ -116,7 +116,7 @@ class PreviewNestedVideoView : PreviewVideoView { if(!_contentSupported) { _containerUnavailable.visibility = View.VISIBLE; _containerLoader.visibility = View.GONE; - _loader.stop(); + _loaderView.stop(); } else { if(_feedStyle == FeedStyle.THUMBNAIL) @@ -132,14 +132,14 @@ class PreviewNestedVideoView : PreviewVideoView { _contentSupported = false; _containerUnavailable.visibility = View.VISIBLE; _containerLoader.visibility = View.GONE; - _loader.stop(); + _loaderView.stop(); } } private fun loadNested(content: IPlatformNestedContent, onCompleted: ((IPlatformContentDetails)->Unit)? = null) { Logger.i(TAG, "Loading nested content [${content.contentUrl}]"); _containerLoader.visibility = View.VISIBLE; - _loader.start(); + _loaderView.start(); StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { val def = StatePlatform.instance.getContentDetails(content.contentUrl); def.invokeOnCompletion { @@ -150,13 +150,13 @@ class PreviewNestedVideoView : PreviewVideoView { if(_content == content) { _containerUnavailable.visibility = View.VISIBLE; _containerLoader.visibility = View.GONE; - _loader.stop(); + _loaderView.stop(); } //TODO: Handle exception } else if(_content == content) { _containerLoader.visibility = View.GONE; - _loader.stop(); + _loaderView.stop(); val nestedContent = def.getCompleted(); _contentNested = nestedContent; if(nestedContent is IPlatformVideoDetails) { diff --git a/app/src/main/java/com/futo/platformplayer/views/overlays/RepliesOverlay.kt b/app/src/main/java/com/futo/platformplayer/views/overlays/RepliesOverlay.kt index 44cb6020..77a0ba88 100644 --- a/app/src/main/java/com/futo/platformplayer/views/overlays/RepliesOverlay.kt +++ b/app/src/main/java/com/futo/platformplayer/views/overlays/RepliesOverlay.kt @@ -4,15 +4,21 @@ import android.content.Context import android.util.AttributeSet import android.view.View import android.widget.LinearLayout +import android.widget.TextView +import androidx.constraintlayout.widget.ConstraintLayout import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.R import com.futo.platformplayer.api.media.models.comments.IPlatformComment import com.futo.platformplayer.api.media.models.comments.PolycentricPlatformComment import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.constructs.Event0 +import com.futo.platformplayer.fixHtmlLinks import com.futo.platformplayer.states.StatePlatform import com.futo.platformplayer.states.StatePolycentric +import com.futo.platformplayer.toHumanNowDiffString +import com.futo.platformplayer.views.behavior.NonScrollingTextView import com.futo.platformplayer.views.comments.AddCommentView +import com.futo.platformplayer.views.others.CreatorThumbnail import com.futo.platformplayer.views.segments.CommentsList import userpackage.Protocol @@ -22,6 +28,11 @@ class RepliesOverlay : LinearLayout { private val _topbar: OverlayTopbar; private val _commentsList: CommentsList; private val _addCommentView: AddCommentView; + private val _textBody: NonScrollingTextView; + private val _textAuthor: TextView; + private val _textMetadata: TextView; + private val _creatorThumbnail: CreatorThumbnail; + private val _layoutParentComment: ConstraintLayout; private var _readonly = false; private var _onCommentAdded: ((comment: IPlatformComment) -> Unit)? = null; @@ -30,6 +41,11 @@ class RepliesOverlay : LinearLayout { _topbar = findViewById(R.id.topbar); _commentsList = findViewById(R.id.comments_list); _addCommentView = findViewById(R.id.add_comment_view); + _textBody = findViewById(R.id.text_body) + _textMetadata = findViewById(R.id.text_metadata) + _textAuthor = findViewById(R.id.text_author) + _creatorThumbnail = findViewById(R.id.image_thumbnail) + _layoutParentComment = findViewById(R.id.layout_parent_comment) _addCommentView.onCommentAdded.subscribe { _commentsList.addComment(it); @@ -42,7 +58,7 @@ class RepliesOverlay : LinearLayout { } } - _commentsList.onClick.subscribe { c -> + _commentsList.onRepliesClick.subscribe { c -> val replyCount = c.replyCount; var metadata = ""; if (replyCount != null && replyCount > 0) { @@ -50,9 +66,9 @@ class RepliesOverlay : LinearLayout { } if (c is PolycentricPlatformComment) { - load(false, metadata, c.contextUrl, c.reference, { StatePolycentric.instance.getCommentPager(c.contextUrl, c.reference) }); + load(false, metadata, c.contextUrl, c.reference, c, { StatePolycentric.instance.getCommentPager(c.contextUrl, c.reference) }); } else { - load(true, metadata, null, null, { StatePlatform.instance.getSubComments(c) }); + load(true, metadata, null, null, c, { StatePlatform.instance.getSubComments(c) }); } }; @@ -60,7 +76,7 @@ class RepliesOverlay : LinearLayout { _topbar.setInfo(context.getString(R.string.Replies), ""); } - fun load(readonly: Boolean, metadata: String, contextUrl: String?, ref: Protocol.Reference?, loader: suspend () -> IPager, onCommentAdded: ((comment: IPlatformComment) -> Unit)? = null) { + fun load(readonly: Boolean, metadata: String, contextUrl: String?, ref: Protocol.Reference?, parentComment: IPlatformComment? = null, loader: suspend () -> IPager, onCommentAdded: ((comment: IPlatformComment) -> Unit)? = null) { _readonly = readonly; if (readonly) { _addCommentView.visibility = View.GONE; @@ -69,6 +85,26 @@ class RepliesOverlay : LinearLayout { _addCommentView.setContext(contextUrl, ref); } + if (parentComment == null) { + _layoutParentComment.visibility = View.GONE + } else { + _layoutParentComment.visibility = View.VISIBLE + + _textBody.text = parentComment.message.fixHtmlLinks() + _textAuthor.text = parentComment.author.name + + val date = parentComment.date + if (date != null) { + _textMetadata.visibility = View.VISIBLE + _textMetadata.text = " • ${date.toHumanNowDiffString()} ago" + } else { + _textMetadata.visibility = View.GONE + } + + _creatorThumbnail.setThumbnail(parentComment.author.thumbnail, false); + _creatorThumbnail.setHarborAvailable(parentComment is PolycentricPlatformComment,false); + } + _topbar.setInfo(context.getString(R.string.Replies), metadata); _commentsList.load(readonly, loader); _onCommentAdded = onCommentAdded; diff --git a/app/src/main/java/com/futo/platformplayer/views/pills/PillButton.kt b/app/src/main/java/com/futo/platformplayer/views/pills/PillButton.kt index 014a24b4..2a84c372 100644 --- a/app/src/main/java/com/futo/platformplayer/views/pills/PillButton.kt +++ b/app/src/main/java/com/futo/platformplayer/views/pills/PillButton.kt @@ -9,16 +9,20 @@ import android.widget.LinearLayout import android.widget.TextView import com.futo.platformplayer.R import com.futo.platformplayer.constructs.Event0 +import com.futo.platformplayer.views.LoaderView class PillButton : LinearLayout { val icon: ImageView; val text: TextView; + val loaderView: LoaderView; val onClick = Event0(); + private var _isLoading = false; constructor(context : Context, attrs : AttributeSet) : super(context, attrs) { LayoutInflater.from(context).inflate(R.layout.pill_button, this, true); icon = findViewById(R.id.pill_icon); text = findViewById(R.id.pill_text); + loaderView = findViewById(R.id.loader) val attrArr = context.obtainStyledAttributes(attrs, R.styleable.PillButton, 0, 0); val attrIconRef = attrArr.getResourceId(R.styleable.PillButton_pillIcon, -1); @@ -31,7 +35,29 @@ class PillButton : LinearLayout { text.text = attrText; findViewById(R.id.root).setOnClickListener { + if (_isLoading) { + return@setOnClickListener + } + onClick.emit(); }; } + + fun setLoading(loading: Boolean) { + if (loading == _isLoading) { + return + } + + if (loading) { + text.visibility = View.GONE + loaderView.visibility = View.VISIBLE + loaderView.start() + } else { + loaderView.stop() + text.visibility = View.VISIBLE + loaderView.visibility = View.GONE + } + + _isLoading = loading + } } \ No newline at end of file diff --git a/app/src/main/java/com/futo/platformplayer/views/pills/PillRatingLikesDislikes.kt b/app/src/main/java/com/futo/platformplayer/views/pills/PillRatingLikesDislikes.kt index f56feced..0266cf5a 100644 --- a/app/src/main/java/com/futo/platformplayer/views/pills/PillRatingLikesDislikes.kt +++ b/app/src/main/java/com/futo/platformplayer/views/pills/PillRatingLikesDislikes.kt @@ -16,6 +16,7 @@ import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.Event3 import com.futo.platformplayer.states.StatePolycentric import com.futo.platformplayer.toHumanNumber +import com.futo.platformplayer.views.LoaderView import com.futo.polycentric.core.ProcessHandle data class OnLikeDislikeUpdatedArgs( @@ -29,9 +30,12 @@ data class OnLikeDislikeUpdatedArgs( class PillRatingLikesDislikes : LinearLayout { private val _textLikes: TextView; private val _textDislikes: TextView; + private val _loaderViewLikes: LoaderView; + private val _loaderViewDislikes: LoaderView; private val _seperator: View; private val _iconLikes: ImageView; private val _iconDislikes: ImageView; + private var _isLoading: Boolean = false; private var _likes = 0L; private var _hasLiked = false; @@ -47,14 +51,42 @@ class PillRatingLikesDislikes : LinearLayout { _seperator = findViewById(R.id.pill_seperator); _iconDislikes = findViewById(R.id.pill_dislike_icon); _iconLikes = findViewById(R.id.pill_like_icon); + _loaderViewLikes = findViewById(R.id.loader_likes) + _loaderViewDislikes = findViewById(R.id.loader_dislikes) - _iconLikes.setOnClickListener { StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_like)) { like(it) }; }; - _textLikes.setOnClickListener { StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_like)) { like(it) }; }; - _iconDislikes.setOnClickListener { StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_dislike)) { dislike(it) }; }; - _textDislikes.setOnClickListener { StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_dislike)) { dislike(it) }; }; + _iconLikes.setOnClickListener { if (!_isLoading) StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_like)) { like(it) }; }; + _textLikes.setOnClickListener { if (!_isLoading) StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_like)) { like(it) }; }; + _iconDislikes.setOnClickListener { if (!_isLoading) StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_dislike)) { dislike(it) }; }; + _textDislikes.setOnClickListener { if (!_isLoading) StatePolycentric.instance.requireLogin(context, context.getString(R.string.please_login_to_dislike)) { dislike(it) }; }; + } + + fun setLoading(loading: Boolean) { + if (_isLoading == loading) { + return + } + + if (loading) { + _textLikes.visibility = View.GONE + _loaderViewLikes.visibility = View.VISIBLE + _textDislikes.visibility = View.GONE + _loaderViewDislikes.visibility = View.VISIBLE + _loaderViewLikes.start() + _loaderViewDislikes.start() + } else { + _loaderViewLikes.stop() + _loaderViewDislikes.stop() + _textLikes.visibility = View.VISIBLE + _loaderViewLikes.visibility = View.GONE + _textDislikes.visibility = View.VISIBLE + _loaderViewDislikes.visibility = View.GONE + } + + _isLoading = loading } fun setRating(rating: IRating, hasLiked: Boolean = false, hasDisliked: Boolean = false) { + setLoading(false) + when (rating) { is RatingLikeDislikes -> { setRating(rating, hasLiked, hasDisliked); diff --git a/app/src/main/java/com/futo/platformplayer/views/segments/CommentsList.kt b/app/src/main/java/com/futo/platformplayer/views/segments/CommentsList.kt index 0677aa99..29adc859 100644 --- a/app/src/main/java/com/futo/platformplayer/views/segments/CommentsList.kt +++ b/app/src/main/java/com/futo/platformplayer/views/segments/CommentsList.kt @@ -19,9 +19,12 @@ import com.futo.platformplayer.api.media.structures.IAsyncPager import com.futo.platformplayer.api.media.structures.IPager import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.TaskHandler -import com.futo.platformplayer.fragment.mainactivity.main.ChannelFragment +import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions +import com.futo.platformplayer.states.StatePolycentric import com.futo.platformplayer.views.adapters.CommentViewHolder import com.futo.platformplayer.views.adapters.InsertedViewAdapterWithLoader +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import java.net.UnknownHostException class CommentsList : ConstraintLayout { @@ -69,7 +72,7 @@ class CommentsList : ConstraintLayout { private val _prependedView: FrameLayout; private var _readonly: Boolean = false; - var onClick = Event1(); + var onRepliesClick = Event1(); var onCommentsLoaded = Event1(); constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { @@ -85,7 +88,8 @@ class CommentsList : ConstraintLayout { childViewHolderBinder = { viewHolder, position -> viewHolder.bind(_comments[position], _readonly); }, childViewHolderFactory = { viewGroup, _ -> val holder = CommentViewHolder(viewGroup); - holder.onClick.subscribe { c -> onClick.emit(c) }; + holder.onRepliesClick.subscribe { c -> onRepliesClick.emit(c) }; + holder.onDelete.subscribe(::onDelete); return@InsertedViewAdapterWithLoader holder; } ); @@ -106,6 +110,36 @@ class CommentsList : ConstraintLayout { _prependedView.addView(view); } + private fun onDelete(comment: IPlatformComment) { + val processHandle = StatePolycentric.instance.processHandle ?: return + if (comment !is PolycentricPlatformComment) { + return + } + + val index = _comments.indexOf(comment) + if (index != -1) { + _comments.removeAt(index) + _adapterComments.notifyItemRemoved(_adapterComments.childToParentPosition(index)) + + StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { + try { + processHandle.delete(comment.eventPointer.process, comment.eventPointer.logicalClock) + } catch (e: Throwable) { + Logger.e(TAG, "Failed to delete event.", e); + return@launch; + } + + try { + Logger.i(TAG, "Started backfill"); + processHandle.fullyBackfillServersAnnounceExceptions(); + Logger.i(TAG, "Finished backfill"); + } catch (e: Throwable) { + Logger.e(TAG, "Failed to fully backfill servers.", e); + } + } + } + } + private fun onScrolled() { val visibleItemCount = _recyclerComments.childCount; val firstVisibleItem = _llmReplies.findFirstVisibleItemPosition(); diff --git a/app/src/main/res/drawable/background_comment.xml b/app/src/main/res/drawable/background_comment.xml new file mode 100644 index 00000000..152c90b9 --- /dev/null +++ b/app/src/main/res/drawable/background_comment.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/background_pill_pred.xml b/app/src/main/res/drawable/background_pill_pred.xml new file mode 100644 index 00000000..85ae3542 --- /dev/null +++ b/app/src/main/res/drawable/background_pill_pred.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_chat_filled.xml b/app/src/main/res/drawable/ic_chat_filled.xml new file mode 100644 index 00000000..dda8bf17 --- /dev/null +++ b/app/src/main/res/drawable/ic_chat_filled.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index 747ad391..ac34014a 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/activity_settings.xml @@ -52,7 +52,7 @@ android:orientation="vertical" android:layout_width="match_parent" android:layout_height="wrap_content"> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_comment.xml b/app/src/main/res/layout/list_comment.xml index 8491b028..3aa9fb8e 100644 --- a/app/src/main/res/layout/list_comment.xml +++ b/app/src/main/res/layout/list_comment.xml @@ -70,7 +70,7 @@ + + + + + + diff --git a/app/src/main/res/layout/list_comment_with_reference.xml b/app/src/main/res/layout/list_comment_with_reference.xml new file mode 100644 index 00000000..2e828255 --- /dev/null +++ b/app/src/main/res/layout/list_comment_with_reference.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_video_thumbnail_nested.xml b/app/src/main/res/layout/list_video_thumbnail_nested.xml index a54e6276..cd2afb51 100644 --- a/app/src/main/res/layout/list_video_thumbnail_nested.xml +++ b/app/src/main/res/layout/list_video_thumbnail_nested.xml @@ -127,7 +127,7 @@ android:visibility="gone" android:gravity="center" android:orientation="vertical"> - diff --git a/app/src/main/res/layout/overlay_replies.xml b/app/src/main/res/layout/overlay_replies.xml index 9ce3174b..c683b38a 100644 --- a/app/src/main/res/layout/overlay_replies.xml +++ b/app/src/main/res/layout/overlay_replies.xml @@ -2,6 +2,7 @@ @@ -15,6 +16,78 @@ app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" /> + + + + + + + + + + + + @@ -32,6 +104,7 @@ android:layout_width="match_parent" android:layout_height="0dp" app:layout_constraintTop_toBottomOf="@id/add_comment_view" - app:layout_constraintBottom_toBottomOf="parent" /> + app:layout_constraintBottom_toBottomOf="parent" + android:layout_marginTop="12dp" /> \ No newline at end of file diff --git a/app/src/main/res/layout/pill_button.xml b/app/src/main/res/layout/pill_button.xml index 759e090f..96457b81 100644 --- a/app/src/main/res/layout/pill_button.xml +++ b/app/src/main/res/layout/pill_button.xml @@ -9,12 +9,13 @@ android:paddingStart="7dp" android:paddingEnd="12dp" android:background="@drawable/background_pill" - android:id="@+id/root"> + android:id="@+id/root" + android:gravity="center_vertical"> + + \ No newline at end of file diff --git a/app/src/main/res/layout/rating_likesdislikes.xml b/app/src/main/res/layout/rating_likesdislikes.xml index c98194e7..a4c6599c 100644 --- a/app/src/main/res/layout/rating_likesdislikes.xml +++ b/app/src/main/res/layout/rating_likesdislikes.xml @@ -8,7 +8,8 @@ android:paddingBottom="7dp" android:paddingLeft="7dp" android:paddingRight="12dp" - android:background="@drawable/background_pill"> + android:background="@drawable/background_pill" + android:gravity="center_vertical"> + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_monetization.xml b/app/src/main/res/layout/view_monetization.xml index aa11779e..d89ee66c 100644 --- a/app/src/main/res/layout/view_monetization.xml +++ b/app/src/main/res/layout/view_monetization.xml @@ -120,7 +120,7 @@ android:orientation="horizontal" android:layout_gravity="center" /> - معلومات تفصيلي + + Newest + Oldest + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 45ba4cec..72d223fb 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -722,4 +722,8 @@ Information Ausführlich + + Newest + Oldest + diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 2e3a05cb..b73d92bf 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -738,4 +738,8 @@ Información Detallado + + Newest + Oldest + diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 5949281b..2691d5d0 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -722,4 +722,8 @@ Information Verbeux + + Newest + Oldest + diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index cb450e4b..c58233ce 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -722,4 +722,8 @@ 情報 詳細 + + Newest + Oldest + diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 10fa7560..2124a56c 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -722,4 +722,8 @@ 정보 상세 + + Newest + Oldest + diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 26f4f3b2..793ff525 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -722,4 +722,8 @@ Informação Detalhado + + Newest + Oldest + \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 9a5b9440..ebc3654b 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -722,4 +722,8 @@ Информация Подробно + + Newest + Oldest + diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 701aee7f..7a0d2f77 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -722,4 +722,8 @@ 信息 详细 + + Newest + Oldest + diff --git a/app/src/main/res/values/loader_attrs.xml b/app/src/main/res/values/loader_attrs.xml index 73aa80fd..0c491998 100644 --- a/app/src/main/res/values/loader_attrs.xml +++ b/app/src/main/res/values/loader_attrs.xml @@ -2,5 +2,6 @@ + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 76b974c8..16bc9d01 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -687,6 +687,7 @@ When you click \'Yes\', the Grayjay app settings will open.\n\nThere, navigate to:\n1. "Open by default" or "Set as default" section.\nYou might find this option directly or under \'Advanced\' settings, depending on your device.\n\n2. Choose \'Open supported links\' for Grayjay.\n\n(some devices have this listed under \'Default Apps\' in the main settings followed by selecting Grayjay for relevant categories) Failed to show settings Play store version does not support default URL handling. + These are all {commentCount} comments you have made in Grayjay. Recommendations Subscriptions @@ -774,6 +775,10 @@ Disabled Enabled + + Newest + Oldest + Name Ascending Name Descending diff --git a/dep/polycentricandroid b/dep/polycentricandroid index 839e4c4a..faaa7a6d 160000 --- a/dep/polycentricandroid +++ b/dep/polycentricandroid @@ -1 +1 @@ -Subproject commit 839e4c4a4f5ed6cb6f68047f88b26c5831e6e703 +Subproject commit faaa7a6d8efb3f92fc239e7d77ec2f9a46c3a958