Compare commits

..
14 Commits
24 changed files with 529 additions and 481 deletions
+1 -1
View File
@@ -60,12 +60,12 @@ Or get the latest APK from the [Releases Section](https://github.com/vitorpamplo
- [x] Audio Tracks (zapstr.live) (NIP-TBD)
- [x] Push Notifications (Zaps and Messages)
- [x] Generic Tags (NIP-12)
- [x] Sensitive Content (NIP-36)
- [ ] Marketplace (NIP-15)
- [ ] Image/Video Capture in the app
- [ ] Local Database
- [ ] View Individual Reactions (Like, Boost, Zaps, Reports) per Post
- [ ] Bookmarks, Pinned Posts, Muted Events (NIP-51)
- [ ] Sensitive Content (NIP-36)
- [ ] Relay Pages (NIP-11)
- [ ] Proof of Work in the Phone (NIP-13, NIP-20)
- [ ] Events with a Subject (NIP-14)
+2 -2
View File
@@ -13,8 +13,8 @@ android {
applicationId "com.vitorpamplona.amethyst"
minSdk 26
targetSdk 33
versionCode 180
versionName "0.52.0"
versionCode 183
versionName "0.52.3"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
@@ -232,7 +232,7 @@ open class Note(val idHex: String) {
fun isZappedBy(user: User, account: Account): Boolean {
// Zaps who the requester was the user
return zaps.any {
it.key.author === user || account.decryptZapContentAuthor(it.key)?.pubKey == user.pubkeyHex
it.key.author?.pubkeyHex == user.pubkeyHex || account.decryptZapContentAuthor(it.key)?.pubKey == user.pubkeyHex
} || zapPayments.any {
val zapResponseEvent = it.value?.event as? LnZapPaymentResponseEvent
val response = if (zapResponseEvent != null) {
@@ -245,11 +245,11 @@ open class Note(val idHex: String) {
}
fun isReactedBy(user: User): Boolean {
return reactions.any { it.author === user }
return reactions.any { it.author?.pubkeyHex == user.pubkeyHex }
}
fun isBoostedBy(user: User): Boolean {
return boosts.any { it.author === user }
return boosts.any { it.author?.pubkeyHex == user.pubkeyHex }
}
fun reportsBy(user: User): Set<Note> {
@@ -299,7 +299,7 @@ class User(val pubkeyHex: String) {
fun hasSentMessagesTo(user: User?): Boolean {
val messagesToUser = privateChatrooms[user] ?: return false
return messagesToUser.roomMessages.any { this === it.author }
return messagesToUser.roomMessages.any { this.pubkeyHex == it.author?.pubkeyHex }
}
fun hasReport(loggedIn: User, type: ReportEvent.ReportType): Boolean {
@@ -63,7 +63,6 @@ import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.AsyncImage
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.NostrSearchEventOrUserDataSource
@@ -78,7 +77,10 @@ import kotlinx.coroutines.launch
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = null, account: Account, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
fun NewPostView(onClose: () -> Unit, baseReplyTo: Note? = null, quote: Note? = null, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = remember(accountState) { accountState?.account } ?: return
val postViewModel: NewPostViewModel = viewModel()
val context = LocalContext.current
@@ -17,18 +17,17 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.ui.actions.NewPostView
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
@Composable
fun NewNoteButton(account: Account, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
fun NewNoteButton(accountViewModel: AccountViewModel, nav: (String) -> Unit) {
var wantsToPost by remember {
mutableStateOf(false)
}
if (wantsToPost) {
NewPostView({ wantsToPost = false }, account = account, accountViewModel = accountViewModel, nav = nav)
NewPostView({ wantsToPost = false }, accountViewModel = accountViewModel, nav = nav)
}
OutlinedButton(
@@ -73,7 +73,7 @@ private fun DisplayEvent(
nip19: Nip19.Return,
nav: (String) -> Unit
) {
var noteBase by remember { mutableStateOf<Note?>(null) }
var noteBase by remember(nip19) { mutableStateOf<Note?>(null) }
LaunchedEffect(key1 = nip19.hex) {
withContext(Dispatchers.IO) {
@@ -129,7 +129,7 @@ private fun DisplayNote(
nip19: Nip19.Return,
nav: (String) -> Unit
) {
var noteBase by remember { mutableStateOf<Note?>(null) }
var noteBase by remember(nip19) { mutableStateOf<Note?>(null) }
LaunchedEffect(key1 = nip19.hex) {
withContext(Dispatchers.IO) {
@@ -185,7 +185,7 @@ private fun DisplayAddress(
nip19: Nip19.Return,
nav: (String) -> Unit
) {
var noteBase by remember { mutableStateOf<Note?>(null) }
var noteBase by remember(nip19) { mutableStateOf<Note?>(null) }
LaunchedEffect(key1 = nip19.hex) {
withContext(Dispatchers.IO) {
@@ -217,7 +217,7 @@ private fun DisplayUser(
nip19: Nip19.Return,
nav: (String) -> Unit
) {
var userBase by remember { mutableStateOf<User?>(null) }
var userBase by remember(nip19) { mutableStateOf<User?>(null) }
LaunchedEffect(key1 = nip19.hex) {
withContext(Dispatchers.IO) {
@@ -227,7 +227,7 @@ private fun DisplayUser(
userBase?.let {
val userState by it.live().metadata.observeAsState()
val route = remember { "User/${it.pubkeyHex}" }
val route = remember(userState) { "User/${it.pubkeyHex}" }
val userDisplayName = remember(userState) { userState?.user?.toBestDisplayName() }
val userTags = remember(userState) { userState?.user?.info?.latestMetadata?.tags }
@@ -95,7 +95,7 @@ fun RichTextViewer(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val isMarkdown = remember { isMarkdown(content) }
val isMarkdown = remember(content) { isMarkdown(content) }
Column(modifier = modifier) {
if (isMarkdown) {
@@ -124,7 +124,7 @@ private fun RenderRegular(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
var state by remember {
var state by remember(content) {
mutableStateOf(RichTextViewerState(content, emptySet(), emptyMap(), emptyList(), emptyMap()))
}
@@ -344,9 +344,9 @@ private fun RenderContentAsMarkdown(content: String, backgroundColor: Color, tag
)
)
var markdownWithSpecialContent by remember { mutableStateOf<String?>(null) }
var nip19References by remember { mutableStateOf<List<Nip19.Return>>(emptyList()) }
var refresh by remember { mutableStateOf(0) }
var markdownWithSpecialContent by remember(content) { mutableStateOf<String?>(null) }
var nip19References by remember(content) { mutableStateOf<List<Nip19.Return>>(emptyList()) }
var refresh by remember(content) { mutableStateOf(0) }
LaunchedEffect(key1 = content) {
withContext(Dispatchers.IO) {
@@ -365,8 +365,8 @@ private fun RenderContentAsMarkdown(content: String, backgroundColor: Color, tag
}
nip19References.forEach {
var baseUser by remember { mutableStateOf<User?>(null) }
var baseNote by remember { mutableStateOf<Note?>(null) }
var baseUser by remember(it) { mutableStateOf<User?>(null) }
var baseNote by remember(it) { mutableStateOf<Note?>(null) }
LaunchedEffect(key1 = it.hex) {
withContext(Dispatchers.IO) {
@@ -21,7 +21,7 @@ import kotlinx.coroutines.launch
fun UrlPreview(url: String, urlText: String) {
val context = LocalContext.current
var urlPreviewState by remember {
var urlPreviewState by remember(url) {
val default = UrlCachedPreviewer.cache[url]?.let {
if (it.allFetchComplete() && it.url == url) {
UrlPreviewState.Loaded(it)
@@ -42,10 +42,10 @@ fun UrlPreviewCard(
)
) {
Column {
val validatedUrl = remember { URL(previewInfo.url) }
val validatedUrl = remember(url) { URL(previewInfo.url) }
// correctly treating relative images
val imageUrl = remember {
val imageUrl = remember(url) {
if (previewInfo.image.startsWith("/")) {
URL(validatedUrl, previewInfo.image).toString()
} else {
@@ -2,9 +2,9 @@ package com.vitorpamplona.amethyst.ui.dal
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.GLOBAL_FOLLOWS
import com.vitorpamplona.amethyst.model.HexKey
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.model.*
object NotificationFeedFilter : AdditiveFeedFilter<Note>() {
@@ -36,7 +36,7 @@ object NotificationFeedFilter : AdditiveFeedFilter<Note>() {
(isGlobal || it.author?.pubkeyHex in followingKeySet) &&
it.event?.isTaggedUser(loggedInUserHex) ?: false &&
(it.author == null || !account.isHidden(it.author!!.pubkeyHex)) &&
tagsAnEventByUser(it, loggedInUser)
tagsAnEventByUser(it, loggedInUserHex)
}.toSet()
}
@@ -44,27 +44,27 @@ object NotificationFeedFilter : AdditiveFeedFilter<Note>() {
return collection.sortedWith(compareBy({ it.createdAt() }, { it.idHex })).reversed()
}
fun tagsAnEventByUser(note: Note, author: User): Boolean {
fun tagsAnEventByUser(note: Note, authorHex: HexKey): Boolean {
val event = note.event
if (event is BaseTextNoteEvent) {
val isAuthoredPostCited = event.findCitations().any {
LocalCache.notes[it]?.author === author || LocalCache.addressables[it]?.author === author
LocalCache.notes[it]?.author?.pubkeyHex == authorHex || LocalCache.addressables[it]?.author?.pubkeyHex == authorHex
}
return isAuthoredPostCited ||
(
event.citedUsers().contains(author.pubkeyHex) ||
note.replyTo?.any { it.author === author } == true
event.citedUsers().contains(authorHex) ||
note.replyTo?.any { it.author?.pubkeyHex == authorHex } == true
)
}
if (event is ReactionEvent) {
return note.replyTo?.lastOrNull()?.author === author
return note.replyTo?.lastOrNull()?.author?.pubkeyHex == authorHex
}
if (event is RepostEvent) {
return note.replyTo?.lastOrNull()?.author === author
return note.replyTo?.lastOrNull()?.author?.pubkeyHex == authorHex
}
return true
@@ -37,8 +37,6 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ColorMatrix
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.layout.onSizeChanged
@@ -66,6 +64,7 @@ import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.RelayIconFilter
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@@ -118,7 +117,7 @@ fun ChatroomMessageCompose(
LaunchedEffect(key1 = noteReportsState, key2 = accountState) {
withContext(Dispatchers.IO) {
account.userProfile().let { loggedIn ->
val newCanPreview = note.author === loggedIn ||
val newCanPreview = note.author?.pubkeyHex == loggedIn.pubkeyHex ||
(note.author?.let { loggedIn.isFollowingCached(it) } ?: true) ||
!(noteForReports.hasAnyReports())
@@ -538,31 +537,27 @@ private fun RelayBadges(baseNote: Note) {
}
@Composable
private fun RenderRelay(dirtyUrl: String) {
fun RenderRelay(dirtyUrl: String) {
val uri = LocalUriHandler.current
val website = remember {
val cleanUrl = dirtyUrl.removePrefix("wss://").removePrefix("ws://")
val website = remember(dirtyUrl) {
val cleanUrl = dirtyUrl.trim().removePrefix("wss://").removePrefix("ws://").removeSuffix("/")
"https://$cleanUrl"
}
val iconUrl = remember {
val cleanUrl = dirtyUrl.removePrefix("wss://").removePrefix("ws://")
val iconUrl = remember(dirtyUrl) {
val cleanUrl = dirtyUrl.trim().removePrefix("wss://").removePrefix("ws://").removeSuffix("/")
"https://$cleanUrl/favicon.ico"
}
val clickableModifier = remember {
val clickableModifier = remember(dirtyUrl) {
Modifier
.size(15.dp)
.padding(1.dp)
.size(15.dp)
.clickable(onClick = { uri.openUri(website) })
}
val colorFilter = remember {
ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0.5f) })
}
val iconModifier = remember {
val iconModifier = remember(dirtyUrl) {
Modifier
.size(15.dp)
.size(13.dp)
.clip(shape = CircleShape)
}
@@ -571,10 +566,10 @@ private fun RenderRelay(dirtyUrl: String) {
) {
RobohashFallbackAsyncImage(
robot = iconUrl,
robotSize = 15.dp,
robotSize = 13.dp,
model = iconUrl,
contentDescription = stringResource(id = R.string.relay_icon),
colorFilter = colorFilter,
colorFilter = RelayIconFilter,
modifier = iconModifier.background(MaterialTheme.colors.background)
)
}
@@ -452,7 +452,7 @@ fun FastNoteAuthorPicture(
val showFollowingMark by remember(accountFollowsState) {
derivedStateOf {
accountFollowsState?.user?.isFollowingCached(author) == true || (author === accountFollowsState?.user)
accountFollowsState?.user?.isFollowingCached(author) == true || (author.pubkeyHex == accountFollowsState?.user?.pubkeyHex)
}
}
@@ -57,14 +57,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ColorMatrix
import androidx.compose.ui.graphics.compositeOver
import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
@@ -86,7 +83,6 @@ import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.NoteState
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.service.model.AudioTrackEvent
import com.vitorpamplona.amethyst.service.model.BadgeAwardEvent
@@ -115,7 +111,6 @@ import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
import com.vitorpamplona.amethyst.ui.components.ResizeImage
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImage
import com.vitorpamplona.amethyst.ui.components.RobohashAsyncImageProxy
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.ui.components.SensitivityWarning
import com.vitorpamplona.amethyst.ui.components.TranslatableRichTextViewer
import com.vitorpamplona.amethyst.ui.components.VideoView
@@ -155,9 +150,9 @@ fun NoteCompose(
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
println("AARender NoteCompose")
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = remember(accountState) { accountState?.account } ?: return
val loggedIn = remember(accountState) { accountState?.account?.userProfile() } ?: return
val noteState by baseNote.live().metadata.observeAsState()
val note = remember(noteState) { noteState?.note } ?: return
@@ -165,13 +160,11 @@ fun NoteCompose(
val noteReportsState by baseNote.live().reports.observeAsState()
val noteForReports = remember(noteReportsState) { noteReportsState?.note } ?: return
val noteEvent = remember(noteState) { note.event }
val baseChannel = remember(noteState) { note.channel() }
val isSensitive = remember(noteState) { note.event?.isSensitive() ?: false }
var popupExpanded by remember { mutableStateOf(false) }
if (note.event == null) {
var popupExpanded by remember { mutableStateOf(false) }
if (noteEvent == null) {
BlankNote(
remember {
modifier.combinedClickable(
@@ -182,19 +175,17 @@ fun NoteCompose(
isBoostedNote
)
note.let {
NoteQuickActionMenu(it, popupExpanded, { popupExpanded = false }, accountViewModel)
}
NoteQuickActionMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel)
} else if (account.isHidden(noteForReports.author!!) || (isSensitive && account.showSensitiveContent == false)) {
// Does nothing
} else {
var showHiddenNote by remember { mutableStateOf(false) }
var showReportedNote by remember { mutableStateOf(false) }
var isAcceptableAndCanPreview by remember { mutableStateOf(Pair(true, true)) }
LaunchedEffect(key1 = noteReportsState, key2 = accountState) {
withContext(Dispatchers.IO) {
account.userProfile().let { loggedIn ->
val newCanPreview = note.author === loggedIn ||
val newCanPreview = note.author?.pubkeyHex == loggedIn.pubkeyHex ||
(note.author?.let { loggedIn.isFollowingCached(it) } ?: true) ||
!(noteForReports.hasAnyReports())
@@ -207,192 +198,236 @@ fun NoteCompose(
}
}
if (!isAcceptableAndCanPreview.first && !showHiddenNote) {
if (!isAcceptableAndCanPreview.first && !showReportedNote) {
HiddenNote(
account.getRelevantReports(noteForReports),
account.userProfile(),
modifier,
isBoostedNote,
nav,
onClick = { showHiddenNote = true }
onClick = { showReportedNote = true }
)
} else if ((noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) && baseChannel != null) {
ChannelHeader(baseChannel = baseChannel, account = account, nav = nav)
} else if (noteEvent is BadgeDefinitionEvent) {
BadgeDisplay(baseNote = note)
} else if (noteEvent is FileHeaderEvent) {
FileHeaderDisplay(note)
} else if (noteEvent is FileStorageHeaderEvent) {
FileStorageHeaderDisplay(note)
} else {
var isNew by remember { mutableStateOf<Boolean>(false) }
NormalNote(
baseNote,
routeForLastRead,
modifier,
isBoostedNote,
isQuotedNote,
unPackReply,
makeItShort,
addMarginTop,
isAcceptableAndCanPreview.second,
parentBackgroundColor,
accountViewModel,
nav
)
}
}
}
val scope = rememberCoroutineScope()
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun NormalNote(
baseNote: Note,
routeForLastRead: String? = null,
modifier: Modifier = Modifier,
isBoostedNote: Boolean = false,
isQuotedNote: Boolean = false,
unPackReply: Boolean = true,
makeItShort: Boolean = false,
addMarginTop: Boolean = true,
canPreview: Boolean = true,
parentBackgroundColor: Color? = null,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
println("AARender NormalNote")
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = remember(accountState) { accountState?.account } ?: return
val loggedIn = remember(accountState) { accountState?.account?.userProfile() } ?: return
LaunchedEffect(key1 = routeForLastRead) {
scope.launch(Dispatchers.IO) {
routeForLastRead?.let {
val lastTime = NotificationCache.load(it)
val noteEvent = remember { baseNote.event }
val channelHex = remember { baseNote.channelHex() }
val createdAt = note.createdAt()
if (createdAt != null) {
NotificationCache.markAsRead(it, createdAt)
var popupExpanded by remember { mutableStateOf(false) }
val newIsNew = createdAt > lastTime
if (newIsNew != isNew) {
isNew = newIsNew
}
if ((noteEvent is ChannelCreateEvent || noteEvent is ChannelMetadataEvent) && channelHex != null) {
ChannelHeader(channelHex = channelHex, account = account, nav = nav)
} else if (noteEvent is BadgeDefinitionEvent) {
BadgeDisplay(baseNote = baseNote)
} else if (noteEvent is FileHeaderEvent) {
FileHeaderDisplay(baseNote)
} else if (noteEvent is FileStorageHeaderEvent) {
FileStorageHeaderDisplay(baseNote)
} else {
var isNew by remember { mutableStateOf<Boolean>(false) }
val scope = rememberCoroutineScope()
LaunchedEffect(key1 = routeForLastRead) {
scope.launch(Dispatchers.IO) {
routeForLastRead?.let {
val lastTime = NotificationCache.load(it)
val createdAt = baseNote.createdAt()
if (createdAt != null) {
NotificationCache.markAsRead(it, createdAt)
val newIsNew = createdAt > lastTime
if (newIsNew != isNew) {
isNew = newIsNew
}
}
}
}
}
val primaryColor = MaterialTheme.colors.primary.copy(0.12f)
val defaultBackgroundColor = MaterialTheme.colors.background
val primaryColor = MaterialTheme.colors.primary.copy(0.12f)
val defaultBackgroundColor = MaterialTheme.colors.background
val backgroundColor = remember(isNew, parentBackgroundColor) {
if (isNew) {
if (parentBackgroundColor != null) {
primaryColor.compositeOver(parentBackgroundColor)
} else {
primaryColor.compositeOver(defaultBackgroundColor)
}
val backgroundColor = remember(isNew, parentBackgroundColor) {
if (isNew) {
if (parentBackgroundColor != null) {
primaryColor.compositeOver(parentBackgroundColor)
} else {
parentBackgroundColor ?: defaultBackgroundColor
primaryColor.compositeOver(defaultBackgroundColor)
}
} else {
parentBackgroundColor ?: defaultBackgroundColor
}
}
val columnModifier = remember(backgroundColor) {
modifier
.combinedClickable(
onClick = {
scope.launch {
routeFor(note, loggedIn)?.let {
nav(it)
}
val columnModifier = remember(backgroundColor) {
modifier
.combinedClickable(
onClick = {
scope.launch {
routeFor(baseNote, loggedIn)?.let {
nav(it)
}
},
onLongClick = { popupExpanded = true }
)
.background(backgroundColor)
}
}
},
onLongClick = { popupExpanded = true }
)
.background(backgroundColor)
}
Column(modifier = columnModifier) {
Row(
Column(modifier = columnModifier) {
Row(
modifier = remember {
Modifier
.padding(
start = if (!isBoostedNote) 12.dp else 0.dp,
end = if (!isBoostedNote) 12.dp else 0.dp,
top = if (addMarginTop && !isBoostedNote) 10.dp else 0.dp
)
}
) {
if (!isBoostedNote && !isQuotedNote) {
DrawAuthorImages(baseNote, loggedIn, nav)
}
Column(
modifier = remember {
Modifier
.padding(
start = if (!isBoostedNote) 12.dp else 0.dp,
end = if (!isBoostedNote) 12.dp else 0.dp,
top = if (addMarginTop && !isBoostedNote) 10.dp else 0.dp
)
.padding(start = if (!isBoostedNote && !isQuotedNote) 10.dp else 0.dp)
}
) {
if (!isBoostedNote && !isQuotedNote) {
DrawAuthorImages(baseNote, loggedIn, nav)
FirstUserInfoRow(
baseNote = baseNote,
showAuthorPicture = isQuotedNote,
account = account,
accountViewModel = accountViewModel,
nav = nav
)
if (noteEvent !is RepostEvent && !makeItShort && !isQuotedNote) {
SecondUserInfoRow(
baseNote,
account,
nav
)
}
Column(
modifier = remember {
Modifier
.padding(start = if (!isBoostedNote && !isQuotedNote) 10.dp else 0.dp)
}
) {
FirstUserInfoRow(
baseNote = baseNote,
showAuthorPicture = isQuotedNote,
account = account,
accountViewModel = accountViewModel,
nav = nav
Spacer(modifier = Modifier.height(2.dp))
if (!makeItShort) {
ReplyRow(
baseNote,
unPackReply,
backgroundColor,
account,
accountViewModel,
nav
)
}
if (noteEvent !is RepostEvent && !makeItShort && !isQuotedNote) {
SecondUserInfoRow(
note,
account,
nav
)
when (noteEvent) {
is ReactionEvent -> {
RenderReaction(baseNote, backgroundColor, accountViewModel, nav)
}
Spacer(modifier = Modifier.height(2.dp))
is RepostEvent -> {
RenderRepost(baseNote, backgroundColor, accountViewModel, nav)
}
if (!makeItShort) {
ReplyRow(
note,
unPackReply,
is ReportEvent -> {
RenderReport(baseNote)
}
is LongTextNoteEvent -> {
RenderLongFormContent(baseNote, loggedIn, accountViewModel, nav)
}
is BadgeAwardEvent -> {
RenderBadgeAward(baseNote, backgroundColor, accountViewModel, nav)
}
is PeopleListEvent -> {
RenderPeopleList(baseNote, backgroundColor, accountViewModel, nav)
}
is AudioTrackEvent -> {
RenderAudioTrack(baseNote, loggedIn, accountViewModel, nav)
}
is PinListEvent -> {
RenderPinListEvent(baseNote, backgroundColor, accountViewModel, nav)
}
is PrivateDmEvent -> {
RenderPrivateMessage(baseNote, makeItShort, canPreview, backgroundColor, accountViewModel, nav)
}
is HighlightEvent -> {
RenderHighlight(baseNote, makeItShort, canPreview, backgroundColor, accountViewModel, nav)
}
is PollNoteEvent -> {
RenderPoll(
baseNote,
makeItShort,
canPreview,
backgroundColor,
account,
accountViewModel,
nav
)
}
when (noteEvent) {
is ReactionEvent -> {
RenderReaction(note, backgroundColor, accountViewModel, nav)
}
is RepostEvent -> {
RenderRepost(note, backgroundColor, accountViewModel, nav)
}
is ReportEvent -> {
RenderReport(note)
}
is LongTextNoteEvent -> {
RenderLongFormContent(note, loggedIn, accountViewModel, nav)
}
is BadgeAwardEvent -> {
RenderBadgeAward(note, backgroundColor, accountViewModel, nav)
}
is PeopleListEvent -> {
RenderPeopleList(noteState, backgroundColor, accountViewModel, nav)
}
is AudioTrackEvent -> {
RenderAudioTrack(note, loggedIn, accountViewModel, nav)
}
is PinListEvent -> {
RenderPinListEvent(noteState, backgroundColor, accountViewModel, nav)
}
is PrivateDmEvent -> {
RenderPrivateMessage(note, makeItShort, isAcceptableAndCanPreview.second, backgroundColor, accountViewModel, nav)
}
is HighlightEvent -> {
RenderHighlight(note, makeItShort, isAcceptableAndCanPreview.second, backgroundColor, accountViewModel, nav)
}
is PollNoteEvent -> {
RenderPoll(
note,
makeItShort,
isAcceptableAndCanPreview.second,
backgroundColor,
accountViewModel,
nav
)
}
else -> {
RenderTextEvent(
note,
makeItShort,
isAcceptableAndCanPreview.second,
backgroundColor,
accountViewModel,
nav
)
}
else -> {
RenderTextEvent(
baseNote,
makeItShort,
canPreview,
backgroundColor,
accountViewModel,
nav
)
}
NoteQuickActionMenu(note, popupExpanded, { popupExpanded = false }, accountViewModel)
}
NoteQuickActionMenu(baseNote, popupExpanded, { popupExpanded = false }, accountViewModel)
}
}
}
@@ -585,14 +620,15 @@ private fun RenderPrivateMessage(
val noteEvent = note.event as? PrivateDmEvent ?: return
val withMe = remember { noteEvent.with(accountViewModel.userProfile().pubkeyHex) }
val tags = remember(note.event?.id()) { note.event?.tags() }
val hashtags = remember(note.event?.id()) { note.event?.hashtags() ?: emptyList() }
val modifier = remember(note.event?.id()) { Modifier.fillMaxWidth() }
val isAuthorTheLoggedUser = remember(note.event?.id()) { accountViewModel.isLoggedUser(note.author) }
if (withMe) {
val eventContent = remember { accountViewModel.decrypt(note) }
val hashtags = remember(note.event?.id()) { note.event?.hashtags() ?: emptyList() }
val modifier = remember(note.event?.id()) { Modifier.fillMaxWidth() }
val isAuthorTheLoggedUser = remember(note.event?.id()) { accountViewModel.isLoggedUser(note.author) }
val tags = remember(note.event?.id()) { note.event?.tags() }
if (eventContent != null) {
if (makeItShort && isAuthorTheLoggedUser) {
Text(
@@ -632,7 +668,7 @@ private fun RenderPrivateMessage(
),
canPreview = !makeItShort,
Modifier.fillMaxWidth(),
noteEvent.tags(),
emptyList(),
backgroundColor,
accountViewModel,
nav
@@ -651,16 +687,14 @@ private fun RenderPrivateMessage(
@Composable
fun RenderPeopleList(
noteState: NoteState?,
baseNote: Note,
backgroundColor: Color,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
DisplayPeopleList(noteState, backgroundColor, accountViewModel, nav)
DisplayPeopleList(baseNote, backgroundColor, accountViewModel, nav)
noteState?.note?.let {
ReactionsRow(it, accountViewModel, nav)
}
ReactionsRow(baseNote, accountViewModel, nav)
Divider(
modifier = Modifier.padding(top = 10.dp),
@@ -670,13 +704,12 @@ fun RenderPeopleList(
@Composable
fun DisplayPeopleList(
noteState: NoteState?,
baseNote: Note,
backgroundColor: Color,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val note = remember(noteState) { noteState?.note } ?: return
val noteEvent = note.event as? PeopleListEvent ?: return
val noteEvent = baseNote.event as? PeopleListEvent ?: return
var members by remember { mutableStateOf<List<User>>(listOf()) }
@@ -702,7 +735,7 @@ fun DisplayPeopleList(
textAlign = TextAlign.Center
)
LaunchedEffect(key1 = noteState) {
LaunchedEffect(Unit) {
withContext(Dispatchers.IO) {
members = noteEvent.bookmarkedPeople().mapNotNull { hex ->
LocalCache.checkGetOrCreateUser(hex)
@@ -879,16 +912,16 @@ private fun RenderRepost(
@Composable
private fun RenderPinListEvent(
noteState: NoteState?,
baseNote: Note,
backgroundColor: Color,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val noteEvent = noteState?.note?.event as? PinListEvent ?: return
val noteEvent = baseNote.event as? PinListEvent ?: return
PinListHeader(noteState, backgroundColor, accountViewModel, nav)
PinListHeader(baseNote, backgroundColor, accountViewModel, nav)
ReactionsRow(noteState?.note, accountViewModel, nav)
ReactionsRow(baseNote, accountViewModel, nav)
Divider(
modifier = Modifier.padding(top = 10.dp),
@@ -898,15 +931,14 @@ private fun RenderPinListEvent(
@Composable
fun PinListHeader(
noteState: NoteState?,
baseNote: Note,
backgroundColor: Color,
accountViewModel: AccountViewModel,
nav: (String) -> Unit
) {
val note = remember(noteState) { noteState?.note } ?: return
val noteEvent = note.event as? PinListEvent ?: return
val noteEvent = baseNote.event as? PinListEvent ?: return
var pins by remember { mutableStateOf<List<String>>(noteEvent.pins()) }
var pins by remember { mutableStateOf(noteEvent.pins()) }
var expanded by remember {
mutableStateOf(false)
@@ -1912,46 +1944,11 @@ private fun VerticalRelayPanelWithFlow(
// FlowRow Seems to be a lot faster than LazyVerticalGrid
FlowRow() {
relays.forEach { url ->
RelayIconCompose(url)
RenderRelay(url)
}
}
}
@Composable
@Stable
private fun RelayIconCompose(url: String) {
val uri = LocalUriHandler.current
val model = remember(url) { "https://$url/favicon.ico" }
val boxModifier = remember {
Modifier
.padding(1.dp)
.size(15.dp)
}
val iconModifier = remember(url) {
Modifier
.width(13.dp)
.height(13.dp)
.clip(shape = CircleShape)
.clickable(onClick = { uri.openUri("https://$url") })
}
val colorFilter = remember {
ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0.5f) })
}
Box(boxModifier) {
RobohashFallbackAsyncImage(
robot = model,
robotSize = 15.dp,
model = model,
contentDescription = stringResource(R.string.relay_icon),
colorFilter = colorFilter,
modifier = iconModifier.background(MaterialTheme.colors.background)
)
}
}
@Composable
private fun ShowMoreRelaysButton(onClick: () -> Unit) {
val boxModifier = remember {
@@ -2069,7 +2066,7 @@ fun UserPicture(
}
val showFollowingMark = remember(accountState) {
accountState?.user?.isFollowingCached(baseUser) == true || baseUser === accountState?.user
accountState?.user?.isFollowingCached(baseUser) == true || baseUser.pubkeyHex == accountState?.user?.pubkeyHex
}
// BaseUser is the same reference as accountState.user
@@ -116,10 +116,10 @@ fun NoteQuickActionMenu(note: Note, popupExpanded: Boolean, onDismiss: () -> Uni
val cardShape = RoundedCornerShape(5.dp)
val clipboardManager = LocalClipboardManager.current
val scope = rememberCoroutineScope()
var showSelectTextDialog by remember { mutableStateOf(false) }
var showDeleteAlertDialog by remember { mutableStateOf(false) }
var showBlockAlertDialog by remember { mutableStateOf(false) }
var showReportDialog by remember { mutableStateOf(false) }
var showSelectTextDialog by remember(note) { mutableStateOf(false) }
var showDeleteAlertDialog by remember(note) { mutableStateOf(false) }
var showBlockAlertDialog by remember(note) { mutableStateOf(false) }
var showReportDialog by remember(note) { mutableStateOf(false) }
val backgroundColor = if (MaterialTheme.colors.isLight) {
MaterialTheme.colors.primary
@@ -1,6 +1,7 @@
package com.vitorpamplona.amethyst.ui.note
import android.widget.Toast
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -20,6 +21,7 @@ import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.ProgressIndicatorDefaults
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Bolt
@@ -56,6 +58,7 @@ import coil.request.CachePolicy
import coil.request.ImageRequest
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.model.User
import com.vitorpamplona.amethyst.ui.actions.NewPostView
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
import com.vitorpamplona.amethyst.ui.theme.BitcoinOrange
@@ -68,9 +71,6 @@ import kotlin.math.roundToInt
@Composable
fun ReactionsRow(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = remember(accountState) { accountState?.account } ?: return
val grayTint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
var wantsToReplyTo by remember {
@@ -82,17 +82,17 @@ fun ReactionsRow(baseNote: Note, accountViewModel: AccountViewModel, nav: (Strin
}
if (wantsToReplyTo != null) {
NewPostView({ wantsToReplyTo = null }, wantsToReplyTo, null, account, accountViewModel, nav)
NewPostView({ wantsToReplyTo = null }, wantsToReplyTo, null, accountViewModel, nav)
}
if (wantsToQuote != null) {
NewPostView({ wantsToQuote = null }, null, wantsToQuote, account, accountViewModel, nav)
NewPostView({ wantsToQuote = null }, null, wantsToQuote, accountViewModel, nav)
}
Spacer(modifier = Modifier.height(8.dp))
Row(verticalAlignment = CenterVertically) {
Row(verticalAlignment = CenterVertically, modifier = Modifier.weight(1f)) {
Row(verticalAlignment = CenterVertically, modifier = remember { Modifier.weight(1f) }) {
ReplyReaction(baseNote, grayTint, accountViewModel) {
wantsToReplyTo = baseNote
}
@@ -123,17 +123,6 @@ fun ReplyReaction(
iconSize: Dp = 20.dp,
onPress: () -> Unit
) {
val repliesState by baseNote.live().replies.observeAsState()
val replies = remember(repliesState) { repliesState?.note?.replies } ?: emptySet()
val isWriteable = remember { accountViewModel.isWriteable() }
val replyCount by remember(repliesState) {
derivedStateOf {
showCount(replies.size)
}
}
val context = LocalContext.current
val scope = rememberCoroutineScope()
@@ -148,7 +137,7 @@ fun ReplyReaction(
IconButton(
modifier = iconButtonModifier,
onClick = {
if (isWriteable) {
if (accountViewModel.isWriteable()) {
onPress()
} else {
scope.launch {
@@ -170,14 +159,26 @@ fun ReplyReaction(
}
if (showCounter) {
Text(
" $replyCount",
fontSize = 14.sp,
color = grayTint
)
ReplyCounter(baseNote, grayTint)
}
}
@Composable
fun ReplyCounter(baseNote: Note, textColor: Color) {
val repliesState by baseNote.live().replies.observeAsState()
val replyCount by remember(repliesState) {
derivedStateOf {
" " + showCount(repliesState?.note?.replies?.size)
}
}
Text(
text = replyCount,
fontSize = 14.sp,
color = textColor
)
}
@Composable
fun BoostReaction(
baseNote: Note,
@@ -186,29 +187,6 @@ fun BoostReaction(
iconSize: Dp = 20.dp,
onQuotePress: () -> Unit
) {
val boostsState by baseNote.live().boosts.observeAsState()
val boostedNote = remember(boostsState) { boostsState?.note } ?: return
val hasBoosted by remember(boostsState) {
derivedStateOf {
accountViewModel.hasBoosted(baseNote)
}
}
val wasBoostedByLoggedIn by remember(boostsState) {
derivedStateOf {
boostedNote.isBoostedBy(accountViewModel.userProfile())
}
}
val isWriteable = remember { accountViewModel.isWriteable() }
val boostCount by remember(boostsState) {
derivedStateOf {
showCount(boostedNote.boosts.size)
}
}
val context = LocalContext.current
val scope = rememberCoroutineScope()
@@ -218,15 +196,11 @@ fun BoostReaction(
Modifier.size(iconSize)
}
val iconModifier = remember {
Modifier.size(iconSize)
}
IconButton(
modifier = iconButtonModifier,
onClick = {
if (isWriteable) {
if (hasBoosted) {
if (accountViewModel.isWriteable()) {
if (accountViewModel.hasBoosted(baseNote)) {
accountViewModel.deleteBoostsTo(baseNote)
} else {
wantsToBoost = true
@@ -256,16 +230,45 @@ fun BoostReaction(
)
}
Icon(
painter = painterResource(R.drawable.ic_retweeted),
null,
modifier = iconModifier,
tint = if (wasBoostedByLoggedIn) Color.Unspecified else grayTint
)
BoostIcon(baseNote, iconSize, grayTint, accountViewModel.userProfile())
}
BoostText(baseNote, grayTint)
}
@Composable
fun BoostIcon(baseNote: Note, iconSize: Dp = 20.dp, grayTint: Color, loggedIn: User) {
val boostsState by baseNote.live().boosts.observeAsState()
val iconTint by remember(boostsState) {
derivedStateOf {
if (boostsState?.note?.isBoostedBy(loggedIn) == true) Color.Unspecified else grayTint
}
}
val iconModifier = remember {
Modifier.size(iconSize)
}
Icon(
painter = painterResource(R.drawable.ic_retweeted),
null,
modifier = iconModifier,
tint = iconTint
)
}
@Composable
fun BoostText(baseNote: Note, grayTint: Color) {
val boostsState by baseNote.live().boosts.observeAsState()
val boostCount by remember(boostsState) {
derivedStateOf {
" " + showCount(boostsState?.note?.boosts?.size)
}
}
Text(
" $boostCount",
boostCount,
fontSize = 14.sp,
color = grayTint
)
@@ -279,29 +282,6 @@ fun LikeReaction(
iconSize: Dp = 20.dp,
heartSize: Dp = 16.dp
) {
val reactionsState by baseNote.live().reactions.observeAsState()
val reactedNote = remember(reactionsState) { reactionsState?.note } ?: return
val hasReacted by remember(reactionsState) {
derivedStateOf {
accountViewModel.hasReactedTo(baseNote)
}
}
val wasReactedByLoggedIn by remember(reactionsState) {
derivedStateOf {
reactedNote.isReactedBy(accountViewModel.userProfile())
}
}
val isWriteable = remember { accountViewModel.isWriteable() }
val reactionCount by remember(reactionsState) {
derivedStateOf {
showCount(reactedNote.reactions.size)
}
}
val context = LocalContext.current
val scope = rememberCoroutineScope()
@@ -309,15 +289,11 @@ fun LikeReaction(
Modifier.size(iconSize)
}
val iconModifier = remember {
Modifier.size(heartSize)
}
IconButton(
modifier = iconButtonModifier,
onClick = {
if (isWriteable) {
if (hasReacted) {
if (accountViewModel.isWriteable()) {
if (accountViewModel.hasReactedTo(baseNote)) {
accountViewModel.deleteReactionTo(baseNote)
} else {
accountViewModel.reactTo(baseNote)
@@ -333,25 +309,55 @@ fun LikeReaction(
}
}
) {
if (wasReactedByLoggedIn) {
Icon(
painter = painterResource(R.drawable.ic_liked),
null,
modifier = iconModifier,
tint = Color.Unspecified
)
} else {
Icon(
painter = painterResource(R.drawable.ic_like),
null,
modifier = iconModifier,
tint = grayTint
)
LikeIcon(baseNote, heartSize, grayTint, accountViewModel.userProfile())
}
LikeText(baseNote, grayTint)
}
@Composable
fun LikeIcon(baseNote: Note, iconSize: Dp = 20.dp, grayTint: Color, loggedIn: User) {
val reactionsState by baseNote.live().reactions.observeAsState()
val wasReactedByLoggedIn by remember(reactionsState) {
derivedStateOf {
reactionsState?.note?.isReactedBy(loggedIn) == true
}
}
val iconModifier = remember {
Modifier.size(iconSize)
}
if (wasReactedByLoggedIn) {
Icon(
painter = painterResource(R.drawable.ic_liked),
null,
modifier = iconModifier,
tint = Color.Unspecified
)
} else {
Icon(
painter = painterResource(R.drawable.ic_like),
null,
modifier = iconModifier,
tint = grayTint
)
}
}
@Composable
fun LikeText(baseNote: Note, grayTint: Color) {
val reactionsState by baseNote.live().reactions.observeAsState()
val reactionsCount by remember(reactionsState) {
derivedStateOf {
" " + showCount(reactionsState?.note?.reactions?.size)
}
}
Text(
" $reactionCount",
reactionsCount,
fontSize = 14.sp,
color = grayTint
)
@@ -363,16 +369,12 @@ fun ZapReaction(
baseNote: Note,
grayTint: Color,
accountViewModel: AccountViewModel,
textModifier: Modifier = Modifier,
iconSize: Dp = 20.dp,
animationSize: Dp = 14.dp
) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = remember(accountState) { accountState?.account } ?: return
val zapsState by baseNote.live().zaps.observeAsState()
val zappedNote = remember(zapsState) { zapsState?.note } ?: return
var wantsToZap by remember { mutableStateOf(false) }
var wantsToChangeZapAmount by remember { mutableStateOf(false) }
var wantsToSetCustomZap by remember { mutableStateOf(false) }
@@ -382,32 +384,6 @@ fun ZapReaction(
var zappingProgress by remember { mutableStateOf(0f) }
var wasZappedByLoggedInUser by remember { mutableStateOf(false) }
var zapAmountTxt by remember { mutableStateOf<String>("") }
LaunchedEffect(key1 = zapsState) {
scope.launch(Dispatchers.IO) {
if (!wasZappedByLoggedInUser) {
val newWasZapped = accountViewModel.calculateIfNoteWasZappedByAccount(zappedNote)
if (wasZappedByLoggedInUser != newWasZapped) {
wasZappedByLoggedInUser = newWasZapped
}
}
val newZapAmount = showAmount(account.calculateZappedAmount(zappedNote))
if (newZapAmount != zapAmountTxt) {
zapAmountTxt = newZapAmount
}
if (wasZappedByLoggedInUser) {
if (abs(zappingProgress - 1) < 0.001) {
zappingProgress = 1f
}
}
}
}
Row(
verticalAlignment = CenterVertically,
modifier = Modifier
@@ -498,6 +474,7 @@ fun ZapReaction(
}
)
}
if (wantsToChangeZapAmount) {
UpdateZapAmountDialog({ wantsToChangeZapAmount = false }, account = account)
}
@@ -506,28 +483,93 @@ fun ZapReaction(
ZapCustomDialog({ wantsToSetCustomZap = false }, account = account, accountViewModel, baseNote)
}
if (wasZappedByLoggedInUser) {
Icon(
imageVector = Icons.Default.Bolt,
contentDescription = stringResource(R.string.zaps),
modifier = Modifier.size(iconSize),
tint = BitcoinOrange
if (zappingProgress > 0.00001 && zappingProgress < 0.99999) {
Spacer(Modifier.width(3.dp))
val animatedProgress = animateFloatAsState(
targetValue = zappingProgress,
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec
).value
CircularProgressIndicator(
progress = animatedProgress,
modifier = Modifier.size(animationSize),
strokeWidth = 2.dp
)
} else {
if (zappingProgress < 0.1 || zappingProgress > 0.99) {
Icon(
imageVector = Icons.Outlined.Bolt,
contentDescription = stringResource(id = R.string.zaps),
modifier = Modifier.size(iconSize),
tint = grayTint
)
} else {
Spacer(Modifier.width(3.dp))
CircularProgressIndicator(
progress = zappingProgress,
modifier = Modifier.size(animationSize),
strokeWidth = 2.dp
)
ZapIcon(
baseNote,
iconSize,
grayTint,
accountViewModel
)
}
}
ZapAmountText(baseNote, grayTint, accountViewModel)
}
@Composable
private fun ZapIcon(
baseNote: Note,
iconSize: Dp,
grayTint: Color,
accountViewModel: AccountViewModel
) {
var wasZappedByLoggedInUser by remember { mutableStateOf(false) }
val zapsState by baseNote.live().zaps.observeAsState()
val scope = rememberCoroutineScope()
LaunchedEffect(key1 = zapsState) {
scope.launch(Dispatchers.IO) {
zapsState?.note?.let {
if (!wasZappedByLoggedInUser) {
val newWasZapped = accountViewModel.calculateIfNoteWasZappedByAccount(it)
if (wasZappedByLoggedInUser != newWasZapped) {
wasZappedByLoggedInUser = newWasZapped
}
}
}
}
}
if (wasZappedByLoggedInUser) {
Icon(
imageVector = Icons.Default.Bolt,
contentDescription = stringResource(R.string.zaps),
modifier = remember { Modifier.size(iconSize) },
tint = BitcoinOrange
)
} else {
Icon(
imageVector = Icons.Outlined.Bolt,
contentDescription = stringResource(id = R.string.zaps),
modifier = remember { Modifier.size(iconSize) },
tint = grayTint
)
}
}
@Composable
private fun ZapAmountText(
baseNote: Note,
grayTint: Color,
accountViewModel: AccountViewModel
) {
val zapsState by baseNote.live().zaps.observeAsState()
val zappedNote = remember(zapsState) { zapsState?.note } ?: return
val scope = rememberCoroutineScope()
var zapAmountTxt by remember { mutableStateOf("") }
LaunchedEffect(key1 = zapsState) {
scope.launch(Dispatchers.IO) {
val newZapAmount = showAmount(accountViewModel.calculateZapAmount(zappedNote))
if (newZapAmount != zapAmountTxt) {
zapAmountTxt = newZapAmount
}
}
}
@@ -535,8 +577,7 @@ fun ZapReaction(
Text(
zapAmountTxt,
fontSize = 14.sp,
color = grayTint,
modifier = textModifier
color = grayTint
)
}
@@ -280,7 +280,7 @@ class UserReactionsViewModel : ViewModel() {
fun UserReplyReaction(
replyCount: Int?
) {
val showCounts = remember { showCount(replyCount) }
val showCounts = remember(replyCount) { showCount(replyCount) }
Icon(
painter = painterResource(R.drawable.ic_comment),
@@ -302,7 +302,7 @@ fun UserReplyReaction(
fun UserBoostReaction(
boostCount: Int?
) {
val showCounts = remember { showCount(boostCount) }
val showCounts = remember(boostCount) { showCount(boostCount) }
Icon(
painter = painterResource(R.drawable.ic_retweeted),
@@ -324,7 +324,7 @@ fun UserBoostReaction(
fun UserLikeReaction(
likeCount: Int?
) {
val showCounts = remember { showCount(likeCount) }
val showCounts = remember(likeCount) { showCount(likeCount) }
Icon(
painter = painterResource(R.drawable.ic_liked),
@@ -346,7 +346,7 @@ fun UserLikeReaction(
fun UserZapReaction(
amount: BigDecimal?
) {
val showAmounts = remember { showAmountAxis(amount) }
val showAmounts = remember(amount) { showAmountAxis(amount) }
Icon(
imageVector = Icons.Default.Bolt,
@@ -353,11 +353,11 @@ fun NoteMaster(
) {
Column() {
if (noteEvent is PeopleListEvent) {
DisplayPeopleList(noteState, MaterialTheme.colors.background, accountViewModel, nav)
DisplayPeopleList(baseNote, MaterialTheme.colors.background, accountViewModel, nav)
} else if (noteEvent is AudioTrackEvent) {
AudioTrackHeader(noteEvent, note, account.userProfile(), nav)
} else if (noteEvent is PinListEvent) {
PinListHeader(noteState, MaterialTheme.colors.background, accountViewModel, nav)
PinListHeader(baseNote, MaterialTheme.colors.background, accountViewModel, nav)
} else if (noteEvent is HighlightEvent) {
DisplayHighlight(
noteEvent.quote(),
@@ -23,6 +23,7 @@ import com.vitorpamplona.amethyst.service.model.ReportEvent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.math.BigDecimal
import java.util.Locale
@Immutable
@@ -66,6 +67,10 @@ class AccountViewModel(private val account: Account) : ViewModel() {
return account.calculateIfNoteWasZappedByAccount(zappedNote)
}
fun calculateZapAmount(zappedNote: Note): BigDecimal {
return account.calculateZappedAmount(zappedNote)
}
fun zap(note: Note, amount: Long, pollOption: Int?, message: String, context: Context, onError: (String) -> Unit, onProgress: (percent: Float) -> Unit, zapType: LnZapEvent.ZapType) {
val lud16 = note.event?.zapAddress() ?: note.author?.info?.lud16?.trim() ?: note.author?.info?.lud06?.trim()
@@ -105,7 +110,7 @@ class AccountViewModel(private val account: Account) : ViewModel() {
?: "Error parsing error message"
)
} else {
onProgress(0.99f)
onProgress(1f)
}
}
)
@@ -210,7 +215,7 @@ class AccountViewModel(private val account: Account) : ViewModel() {
}
fun isLoggedUser(user: User?): Boolean {
return account.userProfile() === user
return account.userProfile().pubkeyHex == user?.pubkeyHex
}
fun isFollowing(user: User?): Boolean {
@@ -40,6 +40,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -63,6 +64,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.model.Account
import com.vitorpamplona.amethyst.model.Channel
import com.vitorpamplona.amethyst.model.LocalCache
import com.vitorpamplona.amethyst.model.Note
import com.vitorpamplona.amethyst.service.NostrChannelDataSource
import com.vitorpamplona.amethyst.ui.actions.NewChannelView
@@ -77,6 +79,8 @@ import com.vitorpamplona.amethyst.ui.navigation.Route
import com.vitorpamplona.amethyst.ui.note.ChatroomMessageCompose
import com.vitorpamplona.amethyst.ui.screen.ChatroomFeedView
import com.vitorpamplona.amethyst.ui.screen.NostrChannelFeedViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable
fun ChannelScreen(
@@ -158,7 +162,12 @@ fun ChannelScreen(
Spacer(modifier = Modifier.height(10.dp))
Row(Modifier.padding(horizontal = 10.dp).animateContentSize(), verticalAlignment = Alignment.CenterVertically) {
Row(
Modifier
.padding(horizontal = 10.dp)
.animateContentSize(),
verticalAlignment = Alignment.CenterVertically
) {
val replyingNote = replyTo.value
if (replyingNote != null) {
Column(Modifier.weight(1f)) {
@@ -182,7 +191,9 @@ fun ChannelScreen(
Icon(
imageVector = Icons.Default.Cancel,
null,
modifier = Modifier.padding(end = 5.dp).size(30.dp),
modifier = Modifier
.padding(end = 5.dp)
.size(30.dp),
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
)
}
@@ -192,7 +203,9 @@ fun ChannelScreen(
// LAST ROW
Row(
modifier = Modifier.padding(start = 10.dp, end = 10.dp, bottom = 10.dp, top = 5.dp).fillMaxWidth(),
modifier = Modifier
.padding(start = 10.dp, end = 10.dp, bottom = 10.dp, top = 5.dp)
.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
@@ -246,10 +259,26 @@ fun ChannelScreen(
}
}
@Composable
fun ChannelHeader(channelHex: String, account: Account, nav: (String) -> Unit) {
var baseChannel by remember { mutableStateOf<Channel?>(null) }
val scope = rememberCoroutineScope()
LaunchedEffect(key1 = channelHex) {
scope.launch(Dispatchers.IO) {
baseChannel = LocalCache.checkGetOrCreateChannel(channelHex)
}
}
baseChannel?.let {
ChannelHeader(it, account, nav)
}
}
@Composable
fun ChannelHeader(baseChannel: Channel, account: Account, nav: (String) -> Unit) {
val channelState by baseChannel.live.observeAsState()
val channel = channelState?.channel ?: return
val channel = remember(channelState) { channelState?.channel } ?: return
val context = LocalContext.current.applicationContext
@@ -123,7 +123,7 @@ fun FloatingButtons(navController: NavHostController, accountViewModel: AccountV
}
is AccountState.LoggedIn -> {
if (currentRoute(navController)?.substringBefore("?") == Route.Home.base) {
NewNoteButton(state.account, accountViewModel, nav)
NewNoteButton(accountViewModel, nav)
}
if (currentRoute(navController) == Route.Message.base) {
ChannelFabColumn(state.account, nav)
@@ -7,7 +7,6 @@ import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -47,12 +46,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ColorMatrix
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -73,7 +68,6 @@ import com.vitorpamplona.amethyst.ui.actions.NewMediaModel
import com.vitorpamplona.amethyst.ui.actions.NewMediaView
import com.vitorpamplona.amethyst.ui.actions.NewPostView
import com.vitorpamplona.amethyst.ui.components.ObserveDisplayNip05Status
import com.vitorpamplona.amethyst.ui.components.RobohashFallbackAsyncImage
import com.vitorpamplona.amethyst.ui.dal.VideoFeedFilter
import com.vitorpamplona.amethyst.ui.navigation.Route
import com.vitorpamplona.amethyst.ui.note.FileHeaderDisplay
@@ -82,6 +76,7 @@ import com.vitorpamplona.amethyst.ui.note.LikeReaction
import com.vitorpamplona.amethyst.ui.note.NoteAuthorPicture
import com.vitorpamplona.amethyst.ui.note.NoteDropDownMenu
import com.vitorpamplona.amethyst.ui.note.NoteUsernameDisplay
import com.vitorpamplona.amethyst.ui.note.RenderRelay
import com.vitorpamplona.amethyst.ui.note.ViewCountReaction
import com.vitorpamplona.amethyst.ui.note.ZapReaction
import com.vitorpamplona.amethyst.ui.screen.FeedEmpty
@@ -317,44 +312,19 @@ private fun RenderVideoOrPictureNote(
@Composable
private fun RelayBadges(baseNote: Note) {
val noteRelaysState by baseNote.live().relays.observeAsState()
val noteRelays = noteRelaysState?.note?.relays ?: emptySet()
var expanded by remember { mutableStateOf(false) }
val relaysToDisplay = noteRelays
val uri = LocalUriHandler.current
val noteRelays = remember(noteRelaysState) {
noteRelaysState?.note?.relays ?: emptySet()
}
FlowRow() {
relaysToDisplay.forEach {
val url = it.removePrefix("wss://").removePrefix("ws://")
Box(
Modifier
.size(15.dp)
.padding(1.dp)
) {
RobohashFallbackAsyncImage(
robot = "https://$url/favicon.ico",
robotSize = 15.dp,
model = "https://$url/favicon.ico",
contentDescription = stringResource(id = R.string.relay_icon),
colorFilter = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0f) }),
modifier = Modifier
.fillMaxSize(1f)
.clip(shape = CircleShape)
.background(MaterialTheme.colors.background)
.clickable(onClick = { uri.openUri("https://$url") })
)
}
noteRelays.forEach { dirtyUrl ->
RenderRelay(dirtyUrl)
}
}
}
@Composable
fun ReactionsColumn(baseNote: Note, accountViewModel: AccountViewModel, nav: (String) -> Unit) {
val accountState by accountViewModel.accountLiveData.observeAsState()
val account = accountState?.account ?: return
var wantsToReplyTo by remember {
mutableStateOf<Note?>(null)
}
@@ -364,11 +334,11 @@ fun ReactionsColumn(baseNote: Note, accountViewModel: AccountViewModel, nav: (St
}
if (wantsToReplyTo != null) {
NewPostView({ wantsToReplyTo = null }, wantsToReplyTo, null, account, accountViewModel, nav)
NewPostView({ wantsToReplyTo = null }, wantsToReplyTo, null, accountViewModel, nav)
}
if (wantsToQuote != null) {
NewPostView({ wantsToQuote = null }, null, wantsToQuote, account, accountViewModel, nav)
NewPostView({ wantsToQuote = null }, null, wantsToQuote, accountViewModel, nav)
}
Spacer(modifier = Modifier.height(8.dp))
@@ -1,6 +1,8 @@
package com.vitorpamplona.amethyst.ui.theme
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.ColorMatrix
val Purple200 = Color(0xFFBB86FC)
val Purple500 = Color(0xFF6200EE)
@@ -15,3 +17,5 @@ val FollowsFollow = Color.Yellow
val NIP05Verified = Color.Blue
val WarningColor = Color(0xFFC62828)
val RelayIconFilter = ColorFilter.colorMatrix(ColorMatrix().apply { setToSaturation(0.5f) })
+6
View File
@@ -385,4 +385,10 @@
<string name="channel_list_join_channel">Csatlakozás</string>
<string name="today">Ma</string>
<string name="content_warning">Tartalomra vonatkozó figyelmeztetés</string>
<string name="content_warning_explanation">Ez a bejegyzés érzékeny tartalmat tartalmaz, amelyet egyesek sértőnek vagy zavarónak találhatnak</string>
<string name="content_warning_hide_all_sensitive_content">Az érzékeny tartalmat mindig rejtse el</string>
<string name="content_warning_show_all_sensitive_content">Az érzékeny tartalmat mindig jelenítse meg</string>
<string name="content_warning_see_warnings">A tartalomra vonatkozó figyelmeztetéseket mindig jelenítse meg</string>
</resources>