Added icon based on the system key when a polycentric user has not set a profile picture. Made managing Polycentric profile most robust. Fixed an issue where latest events were not always being shown in relation to Polycentric profiles. Added copyable (on long press) system key in Polycentric profile activity. Added tutorial videos tab and flow.

This commit is contained in:
Koen 2023-12-20 10:56:16 +01:00
parent 65174ffc97
commit 309a57f5a1
28 changed files with 503 additions and 203 deletions

View File

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

View File

@ -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<ImageButton>(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}'");
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -129,8 +129,8 @@ class TutorialFragment : MainFragment() {
override val dash: IDashManifestSource? = null
override val hls: IHLSManifestSource? = null
override val subtitles: List<ISubtitleSource> = 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")

View File

@ -1201,9 +1201,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) {
@ -1264,57 +1264,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) {
@ -1361,28 +1358,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 = "";
}
}
}
@ -1954,7 +1953,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")
@ -2216,9 +2217,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;
}
@ -2301,7 +2304,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

View File

@ -56,7 +56,7 @@ class PolycentricCache {
private val _taskGetProfile = BatchedTaskHandler<PublicKey, CachedPolycentricProfile>(_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<String, ByteBuffer>(_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
}
}
}

View File

@ -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<IPlatformComment> {
suspend fun getCommentPager(contextUrl: String, reference: Protocol.Reference, extraByteReferences: List<ByteArray>? = null): IPager<IPlatformComment> {
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 };

View File

@ -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<Int> {
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)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -94,4 +94,11 @@
android:text="@string/import_profile" />
</LinearLayout>
<com.futo.platformplayer.views.overlays.LoaderOverlay
android:id="@+id/loader_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:centerLoader="true"
android:visibility="gone" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -51,6 +51,21 @@
app:layout_constraintLeft_toLeftOf="@id/image_polycentric"
app:layout_constraintRight_toRightOf="@id/image_polycentric" />
<TextView
android:id="@+id/text_system"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="gX0eCWctTm6WHVGot4sMAh7NDAIwWsIM5tRsOz9dX04="
android:fontFamily="@font/inter_regular"
android:textSize="10dp"
android:maxLines="1"
android:ellipsize="middle"
android:textColor="@color/gray_67"
android:layout_marginTop="20dp"
app:layout_constraintTop_toBottomOf="@id/edit_profile_name"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<LinearLayout
android:id="@+id/layout_buttons"
android:layout_width="match_parent"
@ -91,4 +106,11 @@
android:layout_marginTop="8dp"
app:buttonBackground="@drawable/background_big_button_red"/>
</LinearLayout>
<com.futo.platformplayer.views.overlays.LoaderOverlay
android:id="@+id/loader_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:centerLoader="true"
android:visibility="gone" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -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" />
</FrameLayout>

View File

@ -7,6 +7,18 @@
android:layout_height="wrap_content"
android:id="@+id/root">
<com.futo.platformplayer.views.IdenticonView
android:id="@+id/identicon"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintDimensionRatio="1:1"
app:srcCompat="@drawable/ic_futo_logo"
android:background="@drawable/rounded_outline"
android:contentDescription="@string/channel_image"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<ImageView
android:id="@+id/image_channel_thumbnail"
android:layout_width="match_parent"

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="LoaderOverlay">
<attr name="centerLoader" format="boolean" />
</declare-styleable>
</resources>

@ -1 +1 @@
Subproject commit 86cd96c41f15f7f73c091f61800a49f376a38150
Subproject commit 7695198eeaeaaea4726712c460081c411ef67866