Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PostContent with selectable text and clickable URLs #100

Merged
merged 3 commits into from
Feb 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions resources/qml/content/Components/PostContent.ui.qml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import QtQuick.Controls 2.15
import QtQuick.Controls.Material 2.15
import QtQuick.Layouts 1.15
import QtQuick.Window 2.15

import Components 1.0
import Futr 1.0

Expand All @@ -27,12 +28,21 @@ Pane {
spacing: Constants.spacing_xs

// Main post content
Text {
TextEdit {
Layout.fillWidth: true
text: (post.content || "").replace(/nostr:(note|nevent|naddr)1[a-zA-Z0-9]+/g, '').trim()
text: {
let content = (post.content || "").replace(/nostr:(note|nevent|naddr)1[a-zA-Z0-9]+/g, '').trim();
content = content.replace(/</g, '&lt;').replace(/>/g, '&gt;'); // Escape any HTML first
content = content.replace(/(https?:\/\/[^\s]+)/g, '<a href="$1" style="color: ' + Material.accentColor + '">$1</a>');
return content;
}
visible: post.postType === "short_text_note" || post.postType === "quote_repost"
wrapMode: Text.Wrap
color: Material.foreground
readOnly: true
selectByMouse: true
textFormat: TextEdit.RichText
onLinkActivated: (link) => Qt.openUrlExternally(link)
}

Repeater {
Expand Down
10 changes: 5 additions & 5 deletions resources/qml/content/Components/ScrollingListView.ui.qml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ ListView {
property int lastContentY: 0
property int lastContentHeight: 0

focus: true
keyNavigationEnabled: true
keyNavigationWraps: false

onContentHeightChanged: {
// If content height increased and we were at bottom, maintain bottom position
if (contentHeight > lastContentHeight && autoScroll) {
Expand Down Expand Up @@ -65,11 +69,7 @@ ListView {
color: parent.pressed ? Material.scrollBarPressedColor :
parent.hovered ? Material.scrollBarHoveredColor :
Material.scrollBarColor
opacity: parent.active ? 1 : 0

Behavior on opacity {
NumberAnimation { duration: 150 }
}
opacity: 1
}
}
}
71 changes: 36 additions & 35 deletions resources/qml/content/Dialogs/PostDialog.ui.qml
Original file line number Diff line number Diff line change
Expand Up @@ -131,18 +131,50 @@ Dialog {
}
}
}

ColumnLayout {
spacing: Constants.spacing_m
Layout.leftMargin: Constants.spacing_m
Layout.rightMargin: Constants.spacing_m

Text {
text: qsTr("Seen on relays:")
color: Material.secondaryTextColor
font: Constants.smallFontMedium
visible: root.targetPost && root.targetPost.relays.length > 0
}

Repeater {
model: root.targetPost ? root.targetPost.relays : []
delegate: ColumnLayout {
Layout.fillWidth: true
spacing: Constants.spacing_s

Text {
text: modelData
color: Material.foreground
wrapMode: Text.Wrap
Layout.fillWidth: true
}

Rectangle {
Layout.fillWidth: true
height: 1
color: Material.dividerColor
visible: root.targetPost && root.targetPost.relays.length > 1 && index < root.targetPost.relays.length - 1
}
}
}
}
}
}

// Comments sections
ListView {
ScrollingListView {
id: commentsView
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
width: scrollView.width - 20
spacing: Constants.spacing_m
bottomMargin: 0

model: AutoListModel {
id: commentsModel
Expand All @@ -162,37 +194,6 @@ Dialog {
post: modelData
}
}

onCountChanged: {
if (atYEnd) {
Qt.callLater(() => {
positionViewAtEnd()
})
}
}

Component.onCompleted: positionViewAtEnd()


ScrollBar.vertical: ScrollBar {
id: scrollBar
active: true
interactive: true
policy: ScrollBar.AlwaysOn

contentItem: Rectangle {
implicitWidth: 6
radius: width / 2
color: scrollBar.pressed ? Material.scrollBarPressedColor :
scrollBar.hovered ? Material.scrollBarHoveredColor :
Material.scrollBarColor
opacity: scrollBar.active ? 1 : 0

Behavior on opacity {
NumberAnimation { duration: 150 }
}
}
}
}

Item {
Expand Down
80 changes: 37 additions & 43 deletions src/Nostr/Publisher.hs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
module Nostr.Publisher where

import Control.Monad (forM, forM_, void, when)
import Control.Monad (forM, forM_, void)
import Data.List (nub, partition)
import Data.Map.Strict qualified as Map
import Data.Set qualified as Set
import Data.Text (Text)
import Effectful
import Effectful.Concurrent (Concurrent)
import Effectful.Concurrent (Concurrent, threadDelay)
import Effectful.Concurrent.Async (async)
import Effectful.Concurrent.STM (atomically, writeTChan)
import Effectful.Dispatch.Dynamic (interpret)
Expand Down Expand Up @@ -87,12 +87,7 @@ runPublisher = interpret $ \_ -> \case
map getUri outboxCapable ++
concatMap (map getUri) followerRelays

modify $ \st' -> st'
{ publishStatus = Map.insert
(eventId event')
(Map.fromList [(relay, Publishing) | relay <- allTargetRelays])
(publishStatus st')
}
initEventPublishStatus (eventId event') allTargetRelays

existingConnections <- getConnectedRelays
let (existingRelays, newRelays) = partition (`elem` existingConnections) allTargetRelays
Expand All @@ -105,13 +100,8 @@ runPublisher = interpret $ \_ -> \case
then do
writeToChannel event' r
disconnect r
else do
modify $ \st' -> st'
{ publishStatus = Map.adjust
(Map.insert r (Failure "Relay server unreachable"))
(eventId event')
(publishStatus st')
}
else
updateEventRelayStatus (eventId event') r (Failure "Relay server unreachable")

PublishToOutbox event' -> do
void $ putEvent $ EventWithRelays event' Set.empty
Expand All @@ -122,23 +112,13 @@ runPublisher = interpret $ \_ -> \case
generalRelayList <- getGeneralRelays pk
let outboxCapableURIs = map getUri $ filter isOutboxCapable generalRelayList

modify $ \st -> st
{ publishStatus = Map.insert
(eventId event')
(Map.fromList [(relay, Publishing) | relay <- outboxCapableURIs])
(publishStatus st)
}
initEventPublishStatus (eventId event') outboxCapableURIs

forM_ outboxCapableURIs $ \r -> writeToChannel event' r

PublishToRelay event' relayUri' -> do
void $ putEvent $ EventWithRelays event' $ Set.empty
modify $ \st -> st
{ publishStatus = Map.adjust
(\existingMap -> Map.insert relayUri' Publishing existingMap)
(eventId event')
(publishStatus st)
}
updateEventRelayStatus (eventId event') relayUri' Publishing
writeToChannel event' relayUri'

PublishGiftWrap event' senderPk recipientPk -> do
Expand All @@ -151,12 +131,7 @@ runPublisher = interpret $ \_ -> \case
else do
let allRelayURIs = nub $ dmRelayList ++ recipientDMRelays

modify $ \st -> st
{ publishStatus = Map.insert
(eventId event')
(Map.fromList [(relay, Publishing) | relay <- allRelayURIs])
(publishStatus st)
}
initEventPublishStatus (eventId event') allRelayURIs

existingConnections <- getConnectedRelays
let (existingRelays, newRelays) = partition
Expand All @@ -169,14 +144,11 @@ runPublisher = interpret $ \_ -> \case
if connected
then do
writeToChannel event' r
-- Delay disconnect to allow message transmission
threadDelay 1000000 -- 1 second delay
disconnect r
else do
modify $ \st' -> st'
{ publishStatus = Map.adjust
(Map.insert r (Failure "Relay server unreachable"))
(eventId event')
(publishStatus st')
}
else
updateEventRelayStatus (eventId event') r (Failure "Relay server unreachable")

GetPublishResult eventId' -> do
st <- get @RelayPool
Expand All @@ -195,13 +167,35 @@ writeToChannel e r = do
case Map.lookup r (activeConnections st) of
Just rd -> do
atomically $ writeTChan (requestChannel rd) (SendEvent e)
modify $ \st' -> st' { publishStatus = Map.adjust (Map.insert r WaitingForConfirmation) (eventId e) (publishStatus st') }
Nothing -> do
modify $ \st' -> st' { publishStatus = Map.adjust (Map.insert r (Failure "No channel found")) (eventId e) (publishStatus st') }
updateEventRelayStatus (eventId e) r WaitingForConfirmation
Nothing ->
updateEventRelayStatus (eventId e) r (Failure "No active connection")


-- | Get the connected relays
getConnectedRelays :: PublisherEff es => Eff es [RelayURI]
getConnectedRelays = do
st <- get @RelayPool
return $ Map.keys $ Map.filter ((== Connected) . connectionState) (activeConnections st)


-- | Update publish status for an event at a specific relay
updateEventRelayStatus :: PublisherEff es => EventId -> RelayURI -> PublishStatus -> Eff es ()
updateEventRelayStatus eid relay status =
modify $ \st -> st
{ publishStatus = Map.adjust
(Map.insert relay status)
eid
(publishStatus st)
}


-- | Initialize publish status for an event with multiple relays
initEventPublishStatus :: PublisherEff es => EventId -> [RelayURI] -> Eff es ()
initEventPublishStatus eid rs =
modify $ \st -> st
{ publishStatus = Map.insert
eid
(Map.fromList [(relay, Publishing) | relay <- rs])
(publishStatus st)
}
Loading