diff --git a/app/src/main/java/com/futo/platformplayer/Extensions_Polycentric.kt b/app/src/main/java/com/futo/platformplayer/Extensions_Polycentric.kt index 7737df09..9d2553f4 100644 --- a/app/src/main/java/com/futo/platformplayer/Extensions_Polycentric.kt +++ b/app/src/main/java/com/futo/platformplayer/Extensions_Polycentric.kt @@ -1,11 +1,13 @@ package com.futo.platformplayer import com.futo.platformplayer.logging.Logger +import com.futo.platformplayer.polycentric.PolycentricCache import com.futo.platformplayer.states.AnnouncementType import com.futo.platformplayer.states.StateAnnouncement import com.futo.platformplayer.states.StatePlatform -import com.futo.platformplayer.views.adapters.CommentViewHolder import com.futo.polycentric.core.ProcessHandle +import com.futo.polycentric.core.Store +import com.futo.polycentric.core.SystemState import userpackage.Protocol import kotlin.math.abs import kotlin.math.min @@ -47,6 +49,15 @@ fun Protocol.Claim.resolveChannelUrls(): List { } suspend fun ProcessHandle.fullyBackfillServersAnnounceExceptions() { + val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(system)) + if (!systemState.servers.contains(PolycentricCache.STAGING_SERVER)) { + removeServer(PolycentricCache.STAGING_SERVER) + } + + if (!systemState.servers.contains(PolycentricCache.SERVER)) { + removeServer(PolycentricCache.SERVER) + } + val exceptions = fullyBackfillServers() for (pair in exceptions) { val server = pair.key diff --git a/app/src/main/java/com/futo/platformplayer/activities/PolycentricImportProfileActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/PolycentricImportProfileActivity.kt index 5ab51be8..d4a98f58 100644 --- a/app/src/main/java/com/futo/platformplayer/activities/PolycentricImportProfileActivity.kt +++ b/app/src/main/java/com/futo/platformplayer/activities/PolycentricImportProfileActivity.kt @@ -8,12 +8,15 @@ import android.widget.ImageButton import android.widget.LinearLayout import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.lifecycleScope import com.futo.platformplayer.R import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.logging.Logger +import com.futo.platformplayer.polycentric.PolycentricCache import com.futo.platformplayer.setNavigationBarColorAndIcons import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StatePolycentric +import com.futo.platformplayer.views.overlays.LoaderOverlay import com.futo.polycentric.core.KeyPair import com.futo.polycentric.core.Process import com.futo.polycentric.core.ProcessSecret @@ -21,6 +24,9 @@ import com.futo.polycentric.core.SignedEvent import com.futo.polycentric.core.Store import com.futo.polycentric.core.base64UrlToByteArray import com.google.zxing.integration.android.IntentIntegrator +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import userpackage.Protocol import userpackage.Protocol.ExportBundle @@ -29,6 +35,7 @@ class PolycentricImportProfileActivity : AppCompatActivity() { private lateinit var _buttonScanProfile: LinearLayout; private lateinit var _buttonImportProfile: LinearLayout; private lateinit var _editProfile: EditText; + private lateinit var _loaderOverlay: LoaderOverlay; private val _qrCodeResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> val scanResult = IntentIntegrator.parseActivityResult(result.resultCode, result.data) @@ -52,6 +59,7 @@ class PolycentricImportProfileActivity : AppCompatActivity() { _buttonHelp = findViewById(R.id.button_help); _buttonScanProfile = findViewById(R.id.button_scan_profile); _buttonImportProfile = findViewById(R.id.button_import_profile); + _loaderOverlay = findViewById(R.id.loader_overlay); _editProfile = findViewById(R.id.edit_profile); findViewById(R.id.button_back).setOnClickListener { finish(); @@ -94,42 +102,57 @@ class PolycentricImportProfileActivity : AppCompatActivity() { return; } - try { - val data = url.substring("polycentric://".length).base64UrlToByteArray(); - val urlInfo = Protocol.URLInfo.parseFrom(data); - if (urlInfo.urlType != 3L) { - throw Exception("Expected urlInfo struct of type ExportBundle") - } + _loaderOverlay.show() - val exportBundle = ExportBundle.parseFrom(urlInfo.body); - val keyPair = KeyPair.fromProto(exportBundle.keyPair); + lifecycleScope.launch(Dispatchers.IO) { + try { + val data = url.substring("polycentric://".length).base64UrlToByteArray(); + val urlInfo = Protocol.URLInfo.parseFrom(data); + if (urlInfo.urlType != 3L) { + throw Exception("Expected urlInfo struct of type ExportBundle") + } - val existingProcessSecret = Store.instance.getProcessSecret(keyPair.publicKey); - if (existingProcessSecret != null) { - UIDialogs.toast(this, getString(R.string.this_profile_is_already_imported)); - return; - } + val exportBundle = ExportBundle.parseFrom(urlInfo.body); + val keyPair = KeyPair.fromProto(exportBundle.keyPair); - val processSecret = ProcessSecret(keyPair, Process.random()); - Store.instance.addProcessSecret(processSecret); + val existingProcessSecret = Store.instance.getProcessSecret(keyPair.publicKey); + if (existingProcessSecret != null) { + withContext(Dispatchers.Main) { + UIDialogs.toast(this@PolycentricImportProfileActivity, getString(R.string.this_profile_is_already_imported)); + } + return@launch; + } - val processHandle = processSecret.toProcessHandle(); + val processSecret = ProcessSecret(keyPair, Process.random()); + Store.instance.addProcessSecret(processSecret); - for (e in exportBundle.events.eventsList) { - try { - val se = SignedEvent.fromProto(e); - Store.instance.putSignedEvent(se); - } catch (e: Throwable) { - Logger.w(TAG, "Ignored invalid event", e); + val processHandle = processSecret.toProcessHandle(); + + for (e in exportBundle.events.eventsList) { + try { + val se = SignedEvent.fromProto(e); + Store.instance.putSignedEvent(se); + } catch (e: Throwable) { + Logger.w(TAG, "Ignored invalid event", e); + } + } + + StatePolycentric.instance.setProcessHandle(processHandle); + processHandle.fullyBackfillClient(PolycentricCache.SERVER); + withContext(Dispatchers.Main) { + startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java)); + finish(); + } + } catch (e: Throwable) { + Logger.w(TAG, "Failed to import profile", e); + withContext(Dispatchers.Main) { + UIDialogs.toast(this@PolycentricImportProfileActivity, getString(R.string.failed_to_import_profile) + " '${e.message}'"); + } + } finally { + withContext(Dispatchers.Main) { + _loaderOverlay.hide(); } } - - StatePolycentric.instance.setProcessHandle(processHandle); - startActivity(Intent(this@PolycentricImportProfileActivity, PolycentricProfileActivity::class.java)); - finish(); - } catch (e: Throwable) { - Logger.w(TAG, "Failed to import profile", e); - UIDialogs.toast(this, getString(R.string.failed_to_import_profile) + " '${e.message}'"); } } diff --git a/app/src/main/java/com/futo/platformplayer/activities/PolycentricProfileActivity.kt b/app/src/main/java/com/futo/platformplayer/activities/PolycentricProfileActivity.kt index b131b1e8..825ba3d7 100644 --- a/app/src/main/java/com/futo/platformplayer/activities/PolycentricProfileActivity.kt +++ b/app/src/main/java/com/futo/platformplayer/activities/PolycentricProfileActivity.kt @@ -1,6 +1,8 @@ package com.futo.platformplayer.activities import android.app.Activity +import android.content.ClipData +import android.content.ClipboardManager import android.content.ContentResolver import android.content.Context import android.content.Intent @@ -12,6 +14,7 @@ import android.webkit.MimeTypeMap import android.widget.EditText import android.widget.ImageButton import android.widget.ImageView +import android.widget.TextView import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope import com.bumptech.glide.Glide @@ -21,14 +24,16 @@ import com.futo.platformplayer.dp import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions import com.futo.platformplayer.images.GlideHelper.Companion.crossfade import com.futo.platformplayer.logging.Logger +import com.futo.platformplayer.polycentric.PolycentricCache import com.futo.platformplayer.selectBestImage import com.futo.platformplayer.setNavigationBarColorAndIcons import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StatePolycentric import com.futo.platformplayer.views.buttons.BigButton +import com.futo.platformplayer.views.overlays.LoaderOverlay import com.futo.polycentric.core.Store -import com.futo.polycentric.core.Synchronization import com.futo.polycentric.core.SystemState +import com.futo.polycentric.core.toBase64Url import com.futo.polycentric.core.toURLInfoSystemLinkUrl import com.github.dhaval2404.imagepicker.ImagePicker import kotlinx.coroutines.Dispatchers @@ -46,6 +51,8 @@ class PolycentricProfileActivity : AppCompatActivity() { private lateinit var _buttonDelete: BigButton; private lateinit var _username: String; private lateinit var _imagePolycentric: ImageView; + private lateinit var _loaderOverlay: LoaderOverlay; + private lateinit var _textSystem: TextView; private var _avatarUri: Uri? = null; override fun attachBaseContext(newBase: Context?) { @@ -63,28 +70,13 @@ class PolycentricProfileActivity : AppCompatActivity() { _buttonExport = findViewById(R.id.button_export); _buttonLogout = findViewById(R.id.button_logout); _buttonDelete = findViewById(R.id.button_delete); + _loaderOverlay = findViewById(R.id.loader_overlay); + _textSystem = findViewById(R.id.text_system) findViewById(R.id.button_back).setOnClickListener { saveIfRequired(); finish(); }; - lifecycleScope.launch(Dispatchers.IO) { - try { - val processHandle = StatePolycentric.instance.processHandle!!; - Synchronization.fullyBackFillClient(processHandle, processHandle.system, "https://srv1-stg.polycentric.io"); - - withContext(Dispatchers.Main) { - updateUI(); - } - } catch (e: Throwable) { - withContext(Dispatchers.Main) { - UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.failed_to_backfill_client)); - } - } - } - - updateUI(); - _imagePolycentric.setOnClickListener { ImagePicker.with(this) .cropSquare() @@ -120,6 +112,37 @@ class PolycentricProfileActivity : AppCompatActivity() { finish(); }); } + + _textSystem.setOnLongClickListener { + val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip: ClipData = ClipData.newPlainText("system", _textSystem.text) + clipboard.setPrimaryClip(clip) + return@setOnLongClickListener true + } + + updateUI() + + StatePolycentric.instance.processHandle?.let { processHandle -> + _loaderOverlay.show() + + lifecycleScope.launch(Dispatchers.IO) { + try { + processHandle.fullyBackfillClient(PolycentricCache.SERVER) + + withContext(Dispatchers.Main) { + updateUI(); + } + } catch (e: Throwable) { + withContext(Dispatchers.Main) { + UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.failed_to_backfill_client)); + } + } finally { + withContext(Dispatchers.Main) { + _loaderOverlay.hide() + } + } + } + } } private fun saveIfRequired() { @@ -128,13 +151,17 @@ class PolycentricProfileActivity : AppCompatActivity() { var hasChanges = false; val username = _editName.text.toString(); if (username.length < 3) { - UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.name_must_be_at_least_3_characters_long)); + withContext(Dispatchers.Main) { + UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.name_must_be_at_least_3_characters_long)); + } return@launch; } val processHandle = StatePolycentric.instance.processHandle; if (processHandle == null) { - UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.process_handle_unset)); + withContext(Dispatchers.Main) { + UIDialogs.toast(this@PolycentricProfileActivity, getString(R.string.process_handle_unset)); + } return@launch; } @@ -219,6 +246,7 @@ class PolycentricProfileActivity : AppCompatActivity() { private fun updateUI() { val processHandle = StatePolycentric.instance.processHandle!!; val systemState = SystemState.fromStorageTypeSystemState(Store.instance.getSystemState(processHandle.system)) + _textSystem.text = processHandle.system.key.toBase64Url() _username = systemState.username; _editName.text.clear(); _editName.text.append(_username); 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 cc9015eb..f369f481 100644 --- a/app/src/main/java/com/futo/platformplayer/dialogs/CommentDialog.kt +++ b/app/src/main/java/com/futo/platformplayer/dialogs/CommentDialog.kt @@ -6,11 +6,12 @@ import android.graphics.Color import android.os.Bundle import android.text.Editable import android.text.TextWatcher -import android.util.Log import android.view.LayoutInflater import android.view.WindowManager import android.view.inputmethod.InputMethodManager -import android.widget.* +import android.widget.EditText +import android.widget.LinearLayout +import android.widget.TextView import com.futo.platformplayer.R import com.futo.platformplayer.UIDialogs import com.futo.platformplayer.api.media.PlatformID @@ -25,7 +26,11 @@ import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.selectBestImage import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StatePolycentric -import com.futo.polycentric.core.* +import com.futo.polycentric.core.ClaimType +import com.futo.polycentric.core.Store +import com.futo.polycentric.core.SystemState +import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl +import com.futo.polycentric.core.toURLInfoSystemLinkUrl import com.google.android.material.button.MaterialButton import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -93,7 +98,7 @@ class CommentDialog(context: Context?, val contextUrl: String, val ref: Protocol val comment = _editComment.text.toString(); val processHandle = StatePolycentric.instance.processHandle!! - val eventPointer = processHandle.post(comment, null, ref) + val eventPointer = processHandle.post(comment, ref) StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { try { diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt index ded1948b..c2549c84 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/ChannelFragment.kt @@ -465,7 +465,7 @@ class ChannelFragment : MainFragment() { _creatorThumbnail.setThumbnail(avatar, animate); } else { _creatorThumbnail.setThumbnail(channel?.thumbnail, animate); - _creatorThumbnail.setHarborAvailable(profile != null, animate); + _creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto()); } val banner = profile?.systemState?.banner?.selectHighestResolutionImage() 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 3cadb47c..2af009ee 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 @@ -314,8 +314,8 @@ class PostDetailFragment : MainFragment { private fun updatePolycentricRating() { _rating.visibility = View.GONE; - val value = _post?.id?.value ?: _postOverview?.id?.value ?: return; - val ref = Models.referenceFromBuffer(value.toByteArray()); + val ref = Models.referenceFromBuffer((_post?.url ?: _postOverview?.url)?.toByteArray() ?: return) + val extraBytesRef = (_post?.id?.value ?: _postOverview?.id?.value)?.toByteArray() val version = _version; _rating.onLikeDislikeUpdated.remove(this); @@ -333,7 +333,8 @@ class PostDetailFragment : MainFragment { Protocol.QueryReferencesRequestCountLWWElementReferences.newBuilder().setFromType( ContentType.OPINION.value).setValue( ByteString.copyFrom(Opinion.dislike.data)).build() - ) + ), + extraByteReferences = listOfNotNull(extraBytesRef) ); if (version != _version) { @@ -342,8 +343,8 @@ class PostDetailFragment : MainFragment { val likes = queryReferencesResponse.countsList[0]; val dislikes = queryReferencesResponse.countsList[1]; - val hasLiked = StatePolycentric.instance.hasLiked(ref); - val hasDisliked = StatePolycentric.instance.hasDisliked(ref); + val hasLiked = StatePolycentric.instance.hasLiked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasLiked(it) } ?: false*/; + val hasDisliked = StatePolycentric.instance.hasDisliked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasDisliked(it) } ?: false*/; withContext(Dispatchers.Main) { if (version != _version) { @@ -468,9 +469,7 @@ class PostDetailFragment : MainFragment { if (_postOverview == null) { fetchPolycentricProfile(); updatePolycentricRating(); - - val ref = value.id.value?.let { Models.referenceFromBuffer(it.toByteArray()); }; - _addCommentView.setContext(value.url, ref); + _addCommentView.setContext(value.url, Models.referenceFromBuffer(value.url.toByteArray())); } updateCommentType(true); @@ -489,9 +488,7 @@ class PostDetailFragment : MainFragment { _textMeta.text = value.datetime?.toHumanNowDiffString()?.let { "$it ago" } ?: "" //TODO: Include view count? _textContent.text = value.description.fixHtmlWhitespace(); _platformIndicator.setPlatformFromClientID(value.id.pluginId); - - val ref = value.id.value?.let { Models.referenceFromBuffer(it.toByteArray()); }; - _addCommentView.setContext(value.url, ref); + _addCommentView.setContext(value.url, Models.referenceFromBuffer(value.url.toByteArray())); updatePolycentricRating(); fetchPolycentricProfile(); @@ -636,12 +633,12 @@ class PostDetailFragment : MainFragment { if (cachedPolycentricProfile?.profile == null) { _layoutMonetization.visibility = View.GONE; - _creatorThumbnail.setHarborAvailable(false, animate); + _creatorThumbnail.setHarborAvailable(false, animate, null); return; } _layoutMonetization.visibility = View.VISIBLE; - _creatorThumbnail.setHarborAvailable(true, animate); + _creatorThumbnail.setHarborAvailable(true, animate, cachedPolycentricProfile.profile.system.toProto()); } private fun fetchPost() { @@ -665,14 +662,16 @@ class PostDetailFragment : MainFragment { private fun fetchPolycentricComments() { Logger.i(TAG, "fetchPolycentricComments") val post = _post; - val idValue = post?.id?.value - if (idValue == null) { - Logger.w(TAG, "Failed to fetch polycentric comments because id was null") + val ref = (_post?.url ?: _postOverview?.url)?.toByteArray()?.let { Models.referenceFromBuffer(it) } + val extraBytesRef = (_post?.id?.value ?: _postOverview?.id?.value)?.toByteArray() + + if (ref == null) { + Logger.w(TAG, "Failed to fetch polycentric comments because url was not set null") _commentsList.clear(); return } - _commentsList.load(false) { StatePolycentric.instance.getCommentPager(post.url, Models.referenceFromBuffer(idValue.toByteArray())); }; + _commentsList.load(false) { StatePolycentric.instance.getCommentPager(post!!.url, ref, listOfNotNull(extraBytesRef)); }; } private fun updateCommentType(reloadComments: Boolean) { diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/TutorialFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/TutorialFragment.kt index aab3986e..6d949c89 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/TutorialFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/TutorialFragment.kt @@ -129,8 +129,8 @@ class TutorialFragment : MainFragment() { override val dash: IDashManifestSource? = null override val hls: IHLSManifestSource? = null override val subtitles: List = emptyList() - override val shareUrl: String = "" - override val url: String = "" + override val shareUrl: String = videoUrl + override val url: String = videoUrl override val datetime: OffsetDateTime? = OffsetDateTime.parse("2023-12-18T00:00:00Z") override val thumbnails: Thumbnails = Thumbnails(arrayOf(Thumbnail(thumbnailUrl))) override val author: PlatformAuthorLink = PlatformAuthorLink(PlatformID("tutorial", "f422ced6-b551-4b62-818e-27a4f5f4918a"), "Grayjay", "", "https://releases.grayjay.app/tutorials/author.jpeg") 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 de122c1d..c6af94e5 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 @@ -1207,9 +1207,9 @@ class VideoDetailView : ConstraintLayout { }; } - val ref = video.id.value?.let { Models.referenceFromBuffer(it.toByteArray()) }; - _addCommentView.setContext(video.url, ref); - + val ref = Models.referenceFromBuffer(video.url.toByteArray()) + val extraBytesRef = video.id.value?.toByteArray() + _addCommentView.setContext(video.url, ref) _player.setMetadata(video.name, video.author.name); if (video !is TutorialFragment.TutorialVideo) { @@ -1270,57 +1270,54 @@ class VideoDetailView : ConstraintLayout { _rating.onLikeDislikeUpdated.remove(this); - if (ref != null) { - _rating.visibility = View.GONE; + _rating.visibility = View.GONE; - fragment.lifecycleScope.launch(Dispatchers.IO) { - try { - val queryReferencesResponse = ApiMethods.getQueryReferences(PolycentricCache.SERVER, ref, null,null, - 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() - ) - ); + fragment.lifecycleScope.launch(Dispatchers.IO) { + try { + val queryReferencesResponse = ApiMethods.getQueryReferences(PolycentricCache.SERVER, ref, null,null, + 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() + ), + extraByteReferences = listOfNotNull(extraBytesRef) + ); - val likes = queryReferencesResponse.countsList[0]; - val dislikes = queryReferencesResponse.countsList[1]; - val hasLiked = StatePolycentric.instance.hasLiked(ref); - val hasDisliked = StatePolycentric.instance.hasDisliked(ref); + val likes = queryReferencesResponse.countsList[0]; + val dislikes = queryReferencesResponse.countsList[1]; + val hasLiked = StatePolycentric.instance.hasLiked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasLiked(it) } ?: false*/; + val hasDisliked = StatePolycentric.instance.hasDisliked(ref.toByteArray())/* || extraBytesRef?.let { StatePolycentric.instance.hasDisliked(it) } ?: false*/; - withContext(Dispatchers.Main) { - _rating.visibility = View.VISIBLE; - _rating.setRating(RatingLikeDislikes(likes, dislikes), hasLiked, hasDisliked); - _rating.onLikeDislikeUpdated.subscribe(this) { args -> - if (args.hasLiked) { - args.processHandle.opinion(ref, Opinion.like); - } else if (args.hasDisliked) { - args.processHandle.opinion(ref, Opinion.dislike); - } else { - args.processHandle.opinion(ref, Opinion.neutral); + withContext(Dispatchers.Main) { + _rating.visibility = View.VISIBLE; + _rating.setRating(RatingLikeDislikes(likes, dislikes), hasLiked, hasDisliked); + _rating.onLikeDislikeUpdated.subscribe(this) { args -> + if (args.hasLiked) { + args.processHandle.opinion(ref, Opinion.like); + } else if (args.hasDisliked) { + args.processHandle.opinion(ref, Opinion.dislike); + } else { + args.processHandle.opinion(ref, Opinion.neutral); + } + + fragment.lifecycleScope.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) } + } - fragment.lifecycleScope.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(ref, args.hasLiked, args.hasDisliked) - }; - } - } catch (e: Throwable) { - Logger.e(TAG, "Failed to get polycentric likes/dislikes.", e); - _rating.visibility = View.GONE; + StatePolycentric.instance.updateLikeMap(ref, args.hasLiked, args.hasDisliked) + }; } + } catch (e: Throwable) { + Logger.e(TAG, "Failed to get polycentric likes/dislikes.", e); + _rating.visibility = View.GONE; } - } else { - _rating.visibility = View.GONE; } when (video.rating) { @@ -1367,28 +1364,30 @@ class VideoDetailView : ConstraintLayout { updateQueueState(); - fragment.lifecycleScope.launch(Dispatchers.IO) { - val historyItem = getHistoryIndex(videoDetail); + if (video !is TutorialFragment.TutorialVideo) { + fragment.lifecycleScope.launch(Dispatchers.IO) { + val historyItem = getHistoryIndex(videoDetail); - withContext(Dispatchers.Main) { - _historicalPosition = StateHistory.instance.updateHistoryPosition(video, historyItem,false, (toResume.toFloat() / 1000.0f).toLong()); - Logger.i(TAG, "Historical position: $_historicalPosition, last position: $lastPositionMilliseconds"); - if (_historicalPosition > 60 && video.duration - _historicalPosition > 5 && Math.abs(_historicalPosition - lastPositionMilliseconds / 1000) > 5.0) { - _layoutResume.visibility = View.VISIBLE; - _textResume.text = "Resume at ${_historicalPosition.toHumanTime(false)}"; + withContext(Dispatchers.Main) { + _historicalPosition = StateHistory.instance.updateHistoryPosition(video, historyItem,false, (toResume.toFloat() / 1000.0f).toLong()); + Logger.i(TAG, "Historical position: $_historicalPosition, last position: $lastPositionMilliseconds"); + if (_historicalPosition > 60 && video.duration - _historicalPosition > 5 && Math.abs(_historicalPosition - lastPositionMilliseconds / 1000) > 5.0) { + _layoutResume.visibility = View.VISIBLE; + _textResume.text = "Resume at ${_historicalPosition.toHumanTime(false)}"; - _jobHideResume = fragment.lifecycleScope.launch(Dispatchers.Main) { - try { - delay(8000); - _layoutResume.visibility = View.GONE; - _textResume.text = ""; - } catch (e: Throwable) { - Logger.e(TAG, "Failed to set resume changes.", e); + _jobHideResume = fragment.lifecycleScope.launch(Dispatchers.Main) { + try { + delay(8000); + _layoutResume.visibility = View.GONE; + _textResume.text = ""; + } catch (e: Throwable) { + Logger.e(TAG, "Failed to set resume changes.", e); + } } + } else { + _layoutResume.visibility = View.GONE; + _textResume.text = ""; } - } else { - _layoutResume.visibility = View.GONE; - _textResume.text = ""; } } } @@ -1960,7 +1959,9 @@ class VideoDetailView : ConstraintLayout { return } - _commentsList.load(false) { StatePolycentric.instance.getCommentPager(video.url, Models.referenceFromBuffer(idValue.toByteArray())); }; + val ref = Models.referenceFromBuffer(video.url.toByteArray()) + val extraBytesRef = video.id.value?.toByteArray() + _commentsList.load(false) { StatePolycentric.instance.getCommentPager(video.url, ref, listOfNotNull(extraBytesRef)); }; } private fun fetchVideo() { Logger.i(TAG, "fetchVideo") @@ -2222,9 +2223,11 @@ class VideoDetailView : ConstraintLayout { val v = video ?: return; val currentTime = System.currentTimeMillis(); if (updateHistory && (_lastPositionSaveTime == -1L || currentTime - _lastPositionSaveTime > 5000)) { - fragment.lifecycleScope.launch(Dispatchers.IO) { - val history = getHistoryIndex(v); - StateHistory.instance.updateHistoryPosition(v, history, true, (positionMilliseconds.toFloat() / 1000.0f).toLong()); + if (v !is TutorialFragment.TutorialVideo) { + fragment.lifecycleScope.launch(Dispatchers.IO) { + val history = getHistoryIndex(v); + StateHistory.instance.updateHistoryPosition(v, history, true, (positionMilliseconds.toFloat() / 1000.0f).toLong()); + } } _lastPositionSaveTime = currentTime; } @@ -2307,7 +2310,7 @@ class VideoDetailView : ConstraintLayout { _creatorThumbnail.setThumbnail(avatar, animate); } else { _creatorThumbnail.setThumbnail(video?.author?.thumbnail, animate); - _creatorThumbnail.setHarborAvailable(profile != null, animate); + _creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto()); } val username = cachedPolycentricProfile?.profile?.systemState?.username diff --git a/app/src/main/java/com/futo/platformplayer/polycentric/PolycentricCache.kt b/app/src/main/java/com/futo/platformplayer/polycentric/PolycentricCache.kt index c80bc8f4..931d3865 100644 --- a/app/src/main/java/com/futo/platformplayer/polycentric/PolycentricCache.kt +++ b/app/src/main/java/com/futo/platformplayer/polycentric/PolycentricCache.kt @@ -56,7 +56,7 @@ class PolycentricCache { private val _taskGetProfile = BatchedTaskHandler(_scope, { system -> - val signedProfileEvents = ApiMethods.getQueryLatest( + val signedEventsList = ApiMethods.getQueryLatest( SERVER, system.toProto(), listOf( @@ -72,8 +72,9 @@ class PolycentricCache { ContentType.MEMBERSHIP_URLS.value, ContentType.DONATION_DESTINATIONS.value ) - ).eventsList.map { e -> SignedEvent.fromProto(e) } - .groupBy { e -> e.event.contentType } + ).eventsList.map { e -> SignedEvent.fromProto(e) }; + + val signedProfileEvents = signedEventsList.groupBy { e -> e.event.contentType } .map { (_, events) -> events.maxBy { it.event.unixMilliseconds ?: 0 } }; val storageSystemState = StorageTypeSystemState.create() @@ -151,17 +152,7 @@ class PolycentricCache { private val _batchTaskGetData = BatchedTaskHandler(_scope, { - val urlData = if (it.startsWith("polycentric://")) { - it.substring("polycentric://".length) - } else it; - - val urlBytes = urlData.base64UrlToByteArray(); - val urlInfo = Protocol.URLInfo.parseFrom(urlBytes); - if (urlInfo.urlType != 4L) { - throw Exception("Only URLInfoDataLink is supported"); - } - - val dataLink = Protocol.URLInfoDataLink.parseFrom(urlInfo.body); + val dataLink = getDataLinkFromUrl(it) ?: throw Exception("Only URLInfoDataLink is supported"); return@BatchedTaskHandler ApiMethods.getDataFromServerAndReassemble(dataLink); }, { return@BatchedTaskHandler null }, @@ -325,9 +316,10 @@ class PolycentricCache { .build(); private const val TAG = "PolycentricCache" - const val SERVER = "https://srv1-stg.polycentric.io" + const val STAGING_SERVER = "https://srv1-stg.polycentric.io" + const val SERVER = "https://srv1-prod.polycentric.io" private var _instance: PolycentricCache? = null; - private val CACHE_EXPIRATION_SECONDS = 60 * 60 * 3; + private val CACHE_EXPIRATION_SECONDS = 60 * 5; @JvmStatic val instance: PolycentricCache @@ -343,5 +335,20 @@ class PolycentricCache { it._scope.cancel("PolycentricCache finished"); } } + + fun getDataLinkFromUrl(it: String): Protocol.URLInfoDataLink? { + val urlData = if (it.startsWith("polycentric://")) { + it.substring("polycentric://".length) + } else it; + + val urlBytes = urlData.base64UrlToByteArray(); + val urlInfo = Protocol.URLInfo.parseFrom(urlBytes); + if (urlInfo.urlType != 4L) { + return null + } + + val dataLink = Protocol.URLInfoDataLink.parseFrom(urlInfo.body); + return dataLink + } } } \ No newline at end of file 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 be41f217..984c6b4f 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StatePolycentric.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StatePolycentric.kt @@ -27,7 +27,20 @@ import com.futo.platformplayer.resolveChannelUrl import com.futo.platformplayer.selectBestImage import com.futo.platformplayer.stores.FragmentedStorage import com.futo.platformplayer.stores.StringStorage -import com.futo.polycentric.core.* +import com.futo.polycentric.core.ApiMethods +import com.futo.polycentric.core.ClaimType +import com.futo.polycentric.core.ContentType +import com.futo.polycentric.core.Opinion +import com.futo.polycentric.core.ProcessHandle +import com.futo.polycentric.core.PublicKey +import com.futo.polycentric.core.SignedEvent +import com.futo.polycentric.core.SqlLiteDbHelper +import com.futo.polycentric.core.Store +import com.futo.polycentric.core.SystemState +import com.futo.polycentric.core.base64ToByteArray +import com.futo.polycentric.core.systemToURLInfoSystemLinkUrl +import com.futo.polycentric.core.toBase64 +import com.futo.polycentric.core.toURLInfoSystemLinkUrl import com.google.protobuf.ByteString import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred @@ -38,7 +51,6 @@ import userpackage.Protocol import java.time.Instant import java.time.OffsetDateTime import java.time.ZoneOffset -import kotlin.Exception class StatePolycentric { private data class LikeDislikeEntry(val unixMilliseconds: Long, val hasLiked: Boolean, val hasDisliked: Boolean); @@ -128,21 +140,21 @@ class StatePolycentric { _likeDislikeMap[ref.toByteArray().toBase64()] = LikeDislikeEntry(System.currentTimeMillis(), hasLiked, hasDisliked); } - fun hasDisliked(ref: Protocol.Reference): Boolean { + fun hasDisliked(data: ByteArray): Boolean { if (!enabled) { return false } - val entry = _likeDislikeMap[ref.toByteArray().toBase64()] ?: return false; + val entry = _likeDislikeMap[data.toBase64()] ?: return false; return entry.hasDisliked; } - fun hasLiked(ref: Protocol.Reference): Boolean { + fun hasLiked(data: ByteArray): Boolean { if (!enabled) { return false } - val entry = _likeDislikeMap[ref.toByteArray().toBase64()] ?: return false; + val entry = _likeDislikeMap[data.toBase64()] ?: return false; return entry.hasLiked; } @@ -316,7 +328,7 @@ class StatePolycentric { return LikesDislikesReplies(likes, dislikes, replyCount) } - suspend fun getCommentPager(contextUrl: String, reference: Protocol.Reference): IPager { + suspend fun getCommentPager(contextUrl: String, reference: Protocol.Reference, extraByteReferences: List? = null): IPager { if (!enabled) { return EmptyPager() } @@ -338,7 +350,8 @@ class StatePolycentric { Protocol.QueryReferencesRequestCountReferences.newBuilder() .setFromType(ContentType.POST.value) .build()) - .build() + .build(), + extraByteReferences = extraByteReferences ); val results = mapQueryReferences(contextUrl, response); @@ -407,7 +420,8 @@ class StatePolycentric { ContentType.AVATAR.value, ContentType.USERNAME.value ) - ).eventsList.map { e -> SignedEvent.fromProto(e) }; + ).eventsList.map { e -> SignedEvent.fromProto(e) }.groupBy { e -> e.event.contentType } + .map { (_, events) -> events.maxBy { x -> x.event.unixMilliseconds ?: 0 } }; val nameEvent = profileEvents.firstOrNull { e -> e.event.contentType == ContentType.USERNAME.value }; val avatarEvent = profileEvents.firstOrNull { e -> e.event.contentType == ContentType.AVATAR.value }; diff --git a/app/src/main/java/com/futo/platformplayer/views/IdenticonView.kt b/app/src/main/java/com/futo/platformplayer/views/IdenticonView.kt new file mode 100644 index 00000000..fd380b73 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/views/IdenticonView.kt @@ -0,0 +1,117 @@ +package com.futo.platformplayer.views + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Path +import android.util.AttributeSet +import android.view.View +import java.security.MessageDigest + +class IdenticonView(context: Context, attrs: AttributeSet) : View(context, attrs) { + var hashString: String = "default" + set(value) { + field = value + hash = md5(value) + invalidate() + } + + private var hash = ByteArray(16) + + private val path = Path() + private val paint = Paint().apply { + style = Paint.Style.FILL + } + + init { + hashString = "default" + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + val radius = (width.coerceAtMost(height) / 2).toFloat() + val clipPath = path.apply { + reset() + addCircle(width / 2f, height / 2f, radius, Path.Direction.CW) + } + + canvas.clipPath(clipPath) + + val size = width.coerceAtMost(height) / 5 + val colors = generateColorsFromHash(hash) + + for (x in 0 until 5) { + for (y in 0 until 5) { + val shapeIndex = getShapeIndex(x, y, hash) + paint.color = colors[shapeIndex % colors.size] + drawShape(canvas, x, y, size, shapeIndex) + } + } + } + + private fun md5(input: String): ByteArray { + val md = MessageDigest.getInstance("MD5") + return md.digest(input.toByteArray(Charsets.UTF_8)) + } + + private fun generateColorsFromHash(hash: ByteArray): List { + val hue = hash[0].toFloat() / 255f + return listOf( + adjustColor(hue, 0.5f, 0.4f), + adjustColor(hue, 0.5f, 0.8f), + adjustColor(hue, 0.5f, 0.3f, 0.9f), + adjustColor(hue, 0.5f, 0.4f, 0.7f) + ) + } + + private fun getShapeIndex(x: Int, y: Int, hash: ByteArray): Int { + val index = if (x < 3) y else 4 - y + return hash[index].toInt() shr x * 2 and 0x03 + } + + private fun drawShape(canvas: Canvas, x: Int, y: Int, size: Int, shapeIndex: Int) { + val left = x * size.toFloat() + val top = y * size.toFloat() + val path = Path() + + when (shapeIndex) { + 0 -> { + // Square + path.addRect(left, top, left + size, top + size, Path.Direction.CW) + } + 1 -> { + // Circle + val radius = size / 2f + path.addCircle(left + radius, top + radius, radius, Path.Direction.CW) + } + 2 -> { + // Diamond + val halfSize = size / 2f + path.moveTo(left + halfSize, top) + path.lineTo(left + size, top + halfSize) + path.lineTo(left + halfSize, top + size) + path.lineTo(left, top + halfSize) + path.close() + } + 3 -> { + // Triangle + path.moveTo(left + size / 2f, top) + path.lineTo(left + size, top + size) + path.lineTo(left, top + size) + path.close() + } + } + + canvas.drawPath(path, paint) + } + + private fun adjustColor(hue: Float, saturation: Float, lightness: Float, alpha: Float = 1.0f): Int { + val color = Color.HSVToColor(floatArrayOf(hue * 360, saturation, lightness)) + val red = Color.red(color) + val green = Color.green(color) + val blue = Color.blue(color) + return Color.argb((alpha * 255).toInt(), red, green, blue) + } +} 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 68103c99..29980ac4 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 @@ -9,15 +9,21 @@ import android.widget.LinearLayout import android.widget.TextView import androidx.constraintlayout.widget.ConstraintLayout import androidx.recyclerview.widget.RecyclerView.ViewHolder -import com.futo.platformplayer.* +import com.futo.platformplayer.R +import com.futo.platformplayer.Settings 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.api.media.models.ratings.RatingLikes import com.futo.platformplayer.constructs.Event1 +import com.futo.platformplayer.fixHtmlLinks +import com.futo.platformplayer.fullyBackfillServersAnnounceExceptions import com.futo.platformplayer.logging.Logger +import com.futo.platformplayer.setPlatformPlayerLinkMovementMethod import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StatePolycentric +import com.futo.platformplayer.toHumanNowDiffString +import com.futo.platformplayer.toHumanNumber import com.futo.platformplayer.views.others.CreatorThumbnail import com.futo.platformplayer.views.pills.PillButton import com.futo.platformplayer.views.pills.PillRatingLikesDislikes @@ -104,7 +110,8 @@ class CommentViewHolder : ViewHolder { fun bind(comment: IPlatformComment, readonly: Boolean) { _creatorThumbnail.setThumbnail(comment.author.thumbnail, false); - _creatorThumbnail.setHarborAvailable(comment is PolycentricPlatformComment,false); + val polycentricComment = if (comment is PolycentricPlatformComment) comment else null + _creatorThumbnail.setHarborAvailable(polycentricComment != null,false, polycentricComment?.eventPointer?.system?.toProto()); _textAuthor.text = comment.author.name; val date = comment.date; @@ -161,8 +168,8 @@ class CommentViewHolder : ViewHolder { _pillRatingLikesDislikes.visibility = View.VISIBLE; if (comment is PolycentricPlatformComment) { - val hasLiked = StatePolycentric.instance.hasLiked(comment.reference); - val hasDisliked = StatePolycentric.instance.hasDisliked(comment.reference); + val hasLiked = StatePolycentric.instance.hasLiked(comment.reference.toByteArray()); + val hasDisliked = StatePolycentric.instance.hasDisliked(comment.reference.toByteArray()); _pillRatingLikesDislikes.setRating(comment.rating, hasLiked, hasDisliked); } else { _pillRatingLikesDislikes.setRating(comment.rating); 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 index 6da01bd3..f1fc07da 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/CommentWithReferenceViewHolder.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/CommentWithReferenceViewHolder.kt @@ -126,7 +126,8 @@ class CommentWithReferenceViewHolder : ViewHolder { _taskGetLiveComment.cancel() _creatorThumbnail.setThumbnail(comment.author.thumbnail, false); - _creatorThumbnail.setHarborAvailable(comment is PolycentricPlatformComment,false); + val polycentricComment = if (comment is PolycentricPlatformComment) comment else null + _creatorThumbnail.setHarborAvailable(polycentricComment != null,false, polycentricComment?.eventPointer?.system?.toProto()); _textAuthor.text = comment.author.name; val date = comment.date; @@ -168,8 +169,8 @@ class CommentWithReferenceViewHolder : ViewHolder { if (likesDislikesReplies != null) { Log.i(TAG, "updateLikesDislikesReplies set") - val hasLiked = StatePolycentric.instance.hasLiked(c.reference); - val hasDisliked = StatePolycentric.instance.hasDisliked(c.reference); + val hasLiked = StatePolycentric.instance.hasLiked(c.reference.toByteArray()); + val hasDisliked = StatePolycentric.instance.hasDisliked(c.reference.toByteArray()); _pillRatingLikesDislikes.setRating(RatingLikeDislikes(likesDislikesReplies.likes, likesDislikesReplies.dislikes), hasLiked, hasDisliked); _buttonReplies.setLoading(false) diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/PlaylistView.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/PlaylistView.kt index 10335eaf..a35d8147 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/PlaylistView.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/PlaylistView.kt @@ -7,7 +7,7 @@ import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView import com.bumptech.glide.Glide -import com.futo.platformplayer.* +import com.futo.platformplayer.R import com.futo.platformplayer.api.media.PlatformID import com.futo.platformplayer.api.media.models.PlatformAuthorLink import com.futo.platformplayer.api.media.models.contents.IPlatformContent @@ -18,8 +18,8 @@ import com.futo.platformplayer.images.GlideHelper.Companion.crossfade import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.polycentric.PolycentricCache import com.futo.platformplayer.states.StateApp -import com.futo.platformplayer.views.others.CreatorThumbnail import com.futo.platformplayer.views.FeedStyle +import com.futo.platformplayer.views.others.CreatorThumbnail import com.futo.platformplayer.views.platform.PlatformIndicator @@ -149,7 +149,8 @@ open class PlaylistView : LinearLayout { _neopassAnimator?.cancel(); _neopassAnimator = null; - val harborAvailable = claims != null && !claims.ownedClaims.isNullOrEmpty(); + val firstClaim = claims?.ownedClaims?.firstOrNull(); + val harborAvailable = firstClaim != null if (harborAvailable) { _imageNeopassChannel?.visibility = View.VISIBLE if (animate) { @@ -160,7 +161,7 @@ open class PlaylistView : LinearLayout { _imageNeopassChannel?.visibility = View.GONE } - _creatorThumbnail?.setHarborAvailable(harborAvailable, animate) + _creatorThumbnail?.setHarborAvailable(harborAvailable, animate, firstClaim?.system?.toProto()) } companion object { diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/SubscriptionViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/SubscriptionViewHolder.kt index 4ee6f00f..603f79d3 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/SubscriptionViewHolder.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/SubscriptionViewHolder.kt @@ -6,21 +6,18 @@ import android.widget.ImageButton import android.widget.LinearLayout import android.widget.TextView import androidx.recyclerview.widget.RecyclerView.ViewHolder -import com.futo.platformplayer.logging.Logger -import com.futo.platformplayer.polycentric.PolycentricCache import com.futo.platformplayer.R import com.futo.platformplayer.Settings -import com.futo.platformplayer.UIDialogs -import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.api.media.PlatformID -import com.futo.platformplayer.models.Subscription import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.dp +import com.futo.platformplayer.logging.Logger +import com.futo.platformplayer.models.Subscription +import com.futo.platformplayer.polycentric.PolycentricCache import com.futo.platformplayer.selectBestImage -import com.futo.platformplayer.states.StateSubscriptions -import com.futo.platformplayer.toHumanBytesSpeed +import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.toHumanTimeIndicator import com.futo.platformplayer.views.others.CreatorThumbnail import com.futo.platformplayer.views.platform.PlatformIndicator @@ -107,7 +104,7 @@ class SubscriptionViewHolder : ViewHolder { _creatorThumbnail.setThumbnail(avatar, animate); } else { _creatorThumbnail.setThumbnail(this.subscription?.channel?.thumbnail, animate); - _creatorThumbnail.setHarborAvailable(profile != null, animate); + _creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto()); } if (profile != null) { diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewVideoView.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewVideoView.kt index 7d38b4a8..6f94c703 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewVideoView.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/feedtypes/PreviewVideoView.kt @@ -334,7 +334,7 @@ open class PreviewVideoView : LinearLayout { _creatorThumbnail.setThumbnail(avatar, animate); } else { _creatorThumbnail.setThumbnail(content?.author?.thumbnail, animate); - _creatorThumbnail.setHarborAvailable(profile != null, animate); + _creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto()); } } else if (_imageChannel != null) { val dp_28 = 28.dp(context.resources); diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/CreatorBarViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/CreatorBarViewHolder.kt index b1338753..14322506 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/CreatorBarViewHolder.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/CreatorBarViewHolder.kt @@ -1,6 +1,5 @@ package com.futo.platformplayer.views.adapters.viewholders -import android.graphics.Color import android.view.LayoutInflater import android.view.ViewGroup import android.widget.LinearLayout @@ -8,12 +7,10 @@ import android.widget.TextView import com.futo.platformplayer.R import com.futo.platformplayer.api.media.PlatformID import com.futo.platformplayer.api.media.models.channels.IPlatformChannel -import com.futo.platformplayer.api.media.models.channels.SerializedChannel import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.constructs.TaskHandler import com.futo.platformplayer.dp import com.futo.platformplayer.logging.Logger -import com.futo.platformplayer.models.Subscription import com.futo.platformplayer.polycentric.PolycentricCache import com.futo.platformplayer.selectBestImage import com.futo.platformplayer.states.StateApp @@ -76,7 +73,7 @@ class CreatorBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter.AnyVi _creatorThumbnail.setThumbnail(avatar, animate); } else { _creatorThumbnail.setThumbnail(_channel?.thumbnail, animate); - _creatorThumbnail.setHarborAvailable(profile != null, animate); + _creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto()); } if (profile != null) { @@ -148,7 +145,7 @@ class SelectableCreatorBarViewHolder(private val _viewGroup: ViewGroup) : AnyAda _creatorThumbnail.setThumbnail(avatar, animate); } else { _creatorThumbnail.setThumbnail(_channel?.channel?.thumbnail, animate); - _creatorThumbnail.setHarborAvailable(profile != null, animate); + _creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto()); } if (profile != null) { diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/CreatorViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/CreatorViewHolder.kt index 750b4fc2..5c57ffa9 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/CreatorViewHolder.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/CreatorViewHolder.kt @@ -98,7 +98,7 @@ class CreatorViewHolder(private val _viewGroup: ViewGroup, private val _tiny: Bo _creatorThumbnail.setThumbnail(avatar, animate); } else { _creatorThumbnail.setThumbnail(_authorLink?.thumbnail, animate); - _creatorThumbnail.setHarborAvailable(profile != null, animate); + _creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto()); } if (profile != null) { diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/SubscriptionBarViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/SubscriptionBarViewHolder.kt index d92e60ae..2b6350c4 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/SubscriptionBarViewHolder.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/SubscriptionBarViewHolder.kt @@ -77,7 +77,7 @@ class SubscriptionBarViewHolder(private val _viewGroup: ViewGroup) : AnyAdapter. _creatorThumbnail.setThumbnail(avatar, animate); } else { _creatorThumbnail.setThumbnail(_channel?.thumbnail, animate); - _creatorThumbnail.setHarborAvailable(profile != null, animate); + _creatorThumbnail.setHarborAvailable(profile != null, animate, profile?.system?.toProto()); } if (profile != null) { diff --git a/app/src/main/java/com/futo/platformplayer/views/behavior/GestureControlView.kt b/app/src/main/java/com/futo/platformplayer/views/behavior/GestureControlView.kt index f48cd486..f500a87e 100644 --- a/app/src/main/java/com/futo/platformplayer/views/behavior/GestureControlView.kt +++ b/app/src/main/java/com/futo/platformplayer/views/behavior/GestureControlView.kt @@ -18,12 +18,20 @@ import android.widget.TextView import androidx.core.animation.doOnEnd import androidx.core.animation.doOnStart import androidx.core.view.GestureDetectorCompat -import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.R import com.futo.platformplayer.constructs.Event0 import com.futo.platformplayer.constructs.Event1 +import com.futo.platformplayer.logging.Logger import com.futo.platformplayer.views.others.CircularProgressBar -import kotlinx.coroutines.* +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch class GestureControlView : LinearLayout { private val _scope = CoroutineScope(Dispatchers.Main); @@ -95,22 +103,23 @@ class GestureControlView : LinearLayout { if(p0 == null) return false; + val minDistance = Math.min(width, height) if (_isFullScreen && _adjustingBrightness) { - val adjustAmount = (distanceY * 2) / height; + val adjustAmount = (distanceY * 2) / minDistance; _brightnessFactor = (_brightnessFactor + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f); _progressBrightness.progress = _brightnessFactor; onBrightnessAdjusted.emit(_brightnessFactor); } else if (_isFullScreen && _adjustingSound) { - val adjustAmount = (distanceY * 2) / height; + val adjustAmount = (distanceY * 2) / minDistance; _soundFactor = (_soundFactor + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f); _progressSound.progress = _soundFactor; onSoundAdjusted.emit(_soundFactor); } else if (_adjustingFullscreenUp) { - val adjustAmount = (distanceY * 2) / height; + val adjustAmount = (distanceY * 2) / minDistance; _fullScreenFactorUp = (_fullScreenFactorUp + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f); _layoutControlsFullscreen.alpha = _fullScreenFactorUp; } else if (_adjustingFullscreenDown) { - val adjustAmount = (-distanceY * 2) / height; + val adjustAmount = (-distanceY * 2) / minDistance; _fullScreenFactorDown = (_fullScreenFactorDown + adjustAmount).coerceAtLeast(0.0f).coerceAtMost(1.0f); _layoutControlsFullscreen.alpha = _fullScreenFactorDown; } else { diff --git a/app/src/main/java/com/futo/platformplayer/views/others/CreatorThumbnail.kt b/app/src/main/java/com/futo/platformplayer/views/others/CreatorThumbnail.kt index 1d9bffd4..ed6a4fbf 100644 --- a/app/src/main/java/com/futo/platformplayer/views/others/CreatorThumbnail.kt +++ b/app/src/main/java/com/futo/platformplayer/views/others/CreatorThumbnail.kt @@ -12,12 +12,16 @@ import com.bumptech.glide.Glide import com.futo.platformplayer.R import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.images.GlideHelper.Companion.crossfade +import com.futo.platformplayer.polycentric.PolycentricCache +import com.futo.platformplayer.views.IdenticonView +import userpackage.Protocol class CreatorThumbnail : ConstraintLayout { private val _root: ConstraintLayout; private val _imageChannelThumbnail: ImageView; private val _imageNewActivity: ImageView; private val _imageNeoPass: ImageView; + private val _identicon: IdenticonView; private var _harborAnimator: ObjectAnimator? = null; private var _imageAnimator: ObjectAnimator? = null; @@ -28,19 +32,22 @@ class CreatorThumbnail : ConstraintLayout { _root = findViewById(R.id.root); _imageChannelThumbnail = findViewById(R.id.image_channel_thumbnail); + _identicon = findViewById(R.id.identicon); _imageChannelThumbnail.clipToOutline = true; + _imageChannelThumbnail.visibility = View.GONE _imageNewActivity = findViewById(R.id.image_new_activity); _imageNeoPass = findViewById(R.id.image_neopass); if (!isInEditMode) { - setHarborAvailable(false, animate = false); + setHarborAvailable(false, animate = false, system = null); setNewActivity(false); } } fun clear() { + _imageChannelThumbnail.visibility = View.GONE; _imageChannelThumbnail.setImageResource(R.drawable.placeholder_channel_thumbnail); - setHarborAvailable(false, animate = false); + setHarborAvailable(false, animate = false, system = null); setNewActivity(false); } @@ -50,13 +57,24 @@ class CreatorThumbnail : ConstraintLayout { return; } + _imageChannelThumbnail.visibility = View.VISIBLE; + _harborAnimator?.cancel(); _harborAnimator = null; _imageAnimator?.cancel(); _imageAnimator = null; - setHarborAvailable(url.startsWith("polycentric://"), animate); + if (url.startsWith("polycentric://")) { + try { + val dataLink = PolycentricCache.getDataLinkFromUrl(url) + setHarborAvailable(true, animate, dataLink?.system); + } catch (e: Throwable) { + setHarborAvailable(false, animate, null); + } + } else { + setHarborAvailable(false, animate, null); + } if (animate) { Glide.with(_imageChannelThumbnail) @@ -72,7 +90,7 @@ class CreatorThumbnail : ConstraintLayout { } } - fun setHarborAvailable(available: Boolean, animate: Boolean) { + fun setHarborAvailable(available: Boolean, animate: Boolean, system: Protocol.PublicKey?) { _harborAnimator?.cancel(); _harborAnimator = null; @@ -85,6 +103,13 @@ class CreatorThumbnail : ConstraintLayout { } else { _imageNeoPass.visibility = View.GONE; } + + if (system != null) { + _identicon.hashString = system.toString() + _identicon.visibility = View.VISIBLE + } else { + _identicon.visibility = View.GONE + } } fun setChannelImageResource(resource: Int?, animate: Boolean) { diff --git a/app/src/main/java/com/futo/platformplayer/views/overlays/LoaderOverlay.kt b/app/src/main/java/com/futo/platformplayer/views/overlays/LoaderOverlay.kt index 06d43e2c..24028323 100644 --- a/app/src/main/java/com/futo/platformplayer/views/overlays/LoaderOverlay.kt +++ b/app/src/main/java/com/futo/platformplayer/views/overlays/LoaderOverlay.kt @@ -3,6 +3,7 @@ package com.futo.platformplayer.views.overlays import android.content.Context import android.graphics.drawable.Animatable import android.util.AttributeSet +import android.view.Gravity import android.view.View import android.widget.FrameLayout import android.widget.ImageView @@ -16,6 +17,21 @@ class LoaderOverlay(context: Context, attrs: AttributeSet?) : FrameLayout(contex inflate(context, R.layout.overlay_loader, this); _container = findViewById(R.id.container); _loader = findViewById(R.id.loader); + + val centerLoader: Boolean; + if (attrs != null) { + val attrArr = context.obtainStyledAttributes(attrs, R.styleable.LoaderOverlay, 0, 0); + centerLoader = attrArr.getBoolean(R.styleable.LoaderOverlay_centerLoader, false); + attrArr.recycle(); + } else { + centerLoader = false; + } + + if (centerLoader) { + (_loader.layoutParams as LayoutParams).apply { + gravity = Gravity.CENTER + } + } } fun show() { 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 77a0ba88..4d2a7fb9 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 @@ -6,8 +6,8 @@ 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.UIDialogs 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 @@ -102,7 +102,8 @@ class RepliesOverlay : LinearLayout { } _creatorThumbnail.setThumbnail(parentComment.author.thumbnail, false); - _creatorThumbnail.setHarborAvailable(parentComment is PolycentricPlatformComment,false); + val polycentricPlatformComment = if (parentComment is PolycentricPlatformComment) parentComment else null + _creatorThumbnail.setHarborAvailable(polycentricPlatformComment != null,false, polycentricPlatformComment?.eventPointer?.system?.toProto()); } _topbar.setInfo(context.getString(R.string.Replies), metadata); diff --git a/app/src/main/res/layout/activity_polycentric_import_profile.xml b/app/src/main/res/layout/activity_polycentric_import_profile.xml index 1b4eac0a..992395cf 100644 --- a/app/src/main/res/layout/activity_polycentric_import_profile.xml +++ b/app/src/main/res/layout/activity_polycentric_import_profile.xml @@ -94,4 +94,11 @@ android:text="@string/import_profile" /> + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_polycentric_profile.xml b/app/src/main/res/layout/activity_polycentric_profile.xml index d69f1606..97c9af8d 100644 --- a/app/src/main/res/layout/activity_polycentric_profile.xml +++ b/app/src/main/res/layout/activity_polycentric_profile.xml @@ -51,6 +51,21 @@ app:layout_constraintLeft_toLeftOf="@id/image_polycentric" app:layout_constraintRight_toRightOf="@id/image_polycentric" /> + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/overlay_loader.xml b/app/src/main/res/layout/overlay_loader.xml index 0e7a561d..c16a6bda 100644 --- a/app/src/main/res/layout/overlay_loader.xml +++ b/app/src/main/res/layout/overlay_loader.xml @@ -17,5 +17,6 @@ android:layout_gravity="top|center_horizontal" android:alpha="0.7" android:layout_marginTop="80dp" + android:layout_marginBottom="80dp" android:contentDescription="@string/loading" /> \ No newline at end of file diff --git a/app/src/main/res/layout/view_creator_thumbnail.xml b/app/src/main/res/layout/view_creator_thumbnail.xml index b7bdc25b..4dad3e39 100644 --- a/app/src/main/res/layout/view_creator_thumbnail.xml +++ b/app/src/main/res/layout/view_creator_thumbnail.xml @@ -7,6 +7,18 @@ android:layout_height="wrap_content" android:id="@+id/root"> + + + + + + + \ No newline at end of file diff --git a/app/src/stable/assets/sources/nebula b/app/src/stable/assets/sources/nebula index 863d0be1..01270edb 160000 --- a/app/src/stable/assets/sources/nebula +++ b/app/src/stable/assets/sources/nebula @@ -1 +1 @@ -Subproject commit 863d0be1322660c99e4d0cdae0b45d0a5918542d +Subproject commit 01270edbb4b6b4fb004e22fc529bf787c7f5be81 diff --git a/app/src/stable/assets/sources/rumble b/app/src/stable/assets/sources/rumble index b0e35a9b..7d4303c8 160000 --- a/app/src/stable/assets/sources/rumble +++ b/app/src/stable/assets/sources/rumble @@ -1 +1 @@ -Subproject commit b0e35a9b6631fb3279fb38619ecc3eba812e5ed6 +Subproject commit 7d4303c82a1ec06871f0717a606f5c6cb87e78be diff --git a/app/src/stable/assets/sources/youtube b/app/src/stable/assets/sources/youtube index d41cc8e8..13551ab6 160000 --- a/app/src/stable/assets/sources/youtube +++ b/app/src/stable/assets/sources/youtube @@ -1 +1 @@ -Subproject commit d41cc8e848891ef8e949e6d49384b754e7c305c7 +Subproject commit 13551ab67fc8fb1899b5bcbfdec750855b154790 diff --git a/app/src/unstable/assets/sources/rumble b/app/src/unstable/assets/sources/rumble index b0e35a9b..7d4303c8 160000 --- a/app/src/unstable/assets/sources/rumble +++ b/app/src/unstable/assets/sources/rumble @@ -1 +1 @@ -Subproject commit b0e35a9b6631fb3279fb38619ecc3eba812e5ed6 +Subproject commit 7d4303c82a1ec06871f0717a606f5c6cb87e78be diff --git a/dep/polycentricandroid b/dep/polycentricandroid index 86cd96c4..7695198e 160000 --- a/dep/polycentricandroid +++ b/dep/polycentricandroid @@ -1 +1 @@ -Subproject commit 86cd96c41f15f7f73c091f61800a49f376a38150 +Subproject commit 7695198eeaeaaea4726712c460081c411ef67866