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

implement comment functionality #61

Merged
merged 1 commit into from
Jan 30, 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
6 changes: 3 additions & 3 deletions resources/qml/content/Components/PostContent.ui.qml
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,12 @@ Pane {
implicitWidth: 36
implicitHeight: 36
padding: 8
icon.color: post.commentCount > 0 ? Material.primary : Material.secondaryTextColor
icon.color: post.comments.length > 0 ? Material.primary : Material.secondaryTextColor
onClicked: commentClicked()
}
Text {
text: post.commentCount || "0"
color: post.commentCount > 0 ? Material.primary : Material.secondaryTextColor
text: post.comments.length || "0"
color: post.comments.length > 0 ? Material.primary : Material.secondaryTextColor
}
}

Expand Down
59 changes: 57 additions & 2 deletions resources/qml/content/Dialogs/PostDialog.ui.qml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import QtQuick.Layouts 1.15

import Components 1.0
import Futr 1.0
import HsQML.Model 1.0

Dialog {
id: root
Expand Down Expand Up @@ -112,7 +113,7 @@ Dialog {
}

Text {
text: (root.targetPost ? (root.targetPost.commentCount || "0") : "0") + qsTr(" Comments")
text: (root.targetPost ? root.targetPost.comments.length : "0") + qsTr(" Comments")
color: Material.secondaryTextColor
font: Constants.smallFontMedium
}
Expand All @@ -123,12 +124,66 @@ Dialog {

// Comments sections
ListView {
id: commentsView
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
verticalLayoutDirection: ListView.TopToBottom
layoutDirection: Qt.LeftToRight
leftMargin: Constants.spacing_m
rightMargin: Constants.spacing_m
spacing: Constants.spacing_m
bottomMargin: 0

model: AutoListModel {
id: commentsModel
source: root.targetPost ? root.targetPost.comments : []
mode: AutoListModel.ByKey
equalityTest: function (oldItem, newItem) {
return oldItem.id === newItem.id
}
}

delegate: Loader {
active: modelData !== undefined && modelData !== null
width: commentsView.width - commentsView.leftMargin - commentsView.rightMargin
height: active ? item.implicitHeight : 0

sourceComponent: PostContent {
post: modelData
}
}

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

Component.onCompleted: positionViewAtEnd()


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

// @todo comments here
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
5 changes: 5 additions & 0 deletions resources/qml/content/MainContent.ui.qml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ Rectangle {
onMessageSubmitted: function(text) {
comment(targetPost.id, text)
}

onRejected: {
setCurrentPost(null)
}
}

Menu {
Expand Down Expand Up @@ -200,6 +204,7 @@ Rectangle {
if (modelData) {
commentsDialog.targetPost = modelData
commentsDialog.open()
setCurrentPost(modelData.id)
}
}

Expand Down
19 changes: 18 additions & 1 deletion src/Futr.hs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ import Nostr.Bech32
import Nostr.Event ( createComment, createEventDeletion, createFollowList
, createQuoteRepost, createRepost, createRumor, createShortTextNote
)
import Nostr.InboxModel (InboxModel, awaitAtLeastOneConnected, startInboxModel, stopInboxModel)
import Nostr.InboxModel (InboxModel, awaitAtLeastOneConnected, startInboxModel, stopInboxModel, subscribeToCommentsFor, unsubscribeToCommentsFor)
import Nostr.Keys (PubKeyXO, derivePublicKeyXO, keyPairToPubKeyXO, secKeyToKeyPair)
import Nostr.Publisher
import Nostr.RelayConnection (RelayConnection, connect, disconnect)
Expand Down Expand Up @@ -82,6 +82,7 @@ data Futr :: Effect where
Login :: ObjRef () -> Text -> Futr m ()
Search :: ObjRef () -> Text -> Futr m SearchResult
SetCurrentProfile :: Text -> Futr m ()
SetCurrentPost :: Maybe EventId -> Futr m ()
FollowProfile :: Text -> Futr m ()
UnfollowProfile :: Text -> Futr m ()
OpenChat :: PubKeyXO -> Futr m ()
Expand Down Expand Up @@ -182,6 +183,22 @@ runFutr = interpret $ \_ -> \case
logError $ "Invalid npub, cannot set current profile: " <> npub'
return ()

SetCurrentPost eid -> do
previousPost <- gets @AppState currentPost

modify @AppState $ \st -> st { currentPost = eid }

case (eid, previousPost) of
(Just newId, _) -> subscribeToCommentsFor newId
(Nothing, Just oldId) -> do
clearCommentsRef
unsubscribeToCommentsFor oldId
(Nothing, Nothing) -> clearCommentsRef
where
clearCommentsRef =
modify @QtQuickState $ \st ->
st { uiRefs = (uiRefs st) { currentPostCommentsObjRef = Nothing } }

FollowProfile npub' -> do
let targetPK = maybe (error "Invalid bech32 public key") id $ bech32ToPubKeyXO npub'
st <- get @AppState
Expand Down
17 changes: 7 additions & 10 deletions src/Nostr/Event.hs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ createComment originalEvent content' rootScope parentItem relayHint xo t =
UnsignedEvent
{ pubKey' = xo
, createdAt' = t
, kind' = Comment
, kind' = ShortTextNote
, tags' = buildTags rootScope parentItem relayHint
, content' = content'
}
Expand All @@ -85,28 +85,25 @@ createComment originalEvent content' rootScope parentItem relayHint xo t =
rootTags = case root of
Left (ITag val _) ->
[ ITag val Nothing
, KTag (pack $ show $ kind originalEvent)
, KTag (pack $ show $ kindToInt $ kind originalEvent)
]
Right eid ->
[ ETag eid relay Nothing Nothing
, KTag (pack $ show $ kind originalEvent)
[ ETag eid relay (Just Root) Nothing
, KTag (pack $ show $ kindToInt $ kind originalEvent)
]
_ -> error "Invalid root scope tag"

-- Parent tags (for replies)
parentTags = case parent of
Just (ETag eid _ mpk _) ->
[ ETag eid relay mpk Nothing
, KTag (pack $ show Comment)
, KTag (pack $ show $ kindToInt Comment)
]
Just (ITag val _) ->
[ ITag val Nothing
, KTag (pack $ show $ kind originalEvent)
, KTag (pack $ show $ kindToInt $ kind originalEvent)
]
Nothing -> case root of
Left itag@(ITag _ _) -> [itag, KTag (pack $ show Comment)]
Right eid -> [ETag eid relay Nothing Nothing, KTag (pack $ show Comment)]
_ -> []
Nothing -> []
_ -> error "Invalid parent tag"
in
rootTags ++ parentTags
Expand Down
32 changes: 29 additions & 3 deletions src/Nostr/InboxModel.hs
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,13 @@ import Nostr.Subscription
, stopSubscription
, subscribe
, userPostsFilter
, commentsFilter
)
import Nostr.Types
( RelayURI
, Relay(..)
, Event(..)
, EventId
, Filter(..)
, Kind(..)
, SubscriptionId
Expand All @@ -60,6 +62,8 @@ data InboxModel :: Effect where
StartInboxModel :: InboxModel m ()
StopInboxModel :: InboxModel m ()
AwaitAtLeastOneConnected :: InboxModel m Bool
SubscribeToCommentsFor :: EventId -> InboxModel m ()
UnsubscribeToCommentsFor :: EventId -> InboxModel m ()

type instance DispatchOf InboxModel = Dynamic

Expand Down Expand Up @@ -108,16 +112,38 @@ runInboxModel = interpret $ \_ -> \case

StopInboxModel -> do
st <- get @RelayPool
-- Cancel the update thread if it exists

forM_ (updateThread st) cancel

modify @RelayPool $ \s -> s { updateThread = Nothing }
-- Disconnect all relays

forM_ (Map.keys $ activeConnections st) $ \relayUri -> do
disconnect relayUri

put @RelayPool initialRelayPool

AwaitAtLeastOneConnected -> awaitAtLeastOneConnected'

SubscribeToCommentsFor eid -> do
pool <- get @RelayPool
queue <- gets @RelayPool inboxQueue
let activeRelays = Map.keys $ activeConnections pool

subIds <- forM activeRelays $ \relayUri -> do
subscribe relayUri (commentsFilter eid) queue

modify @RelayPool $ \s ->
s { commentSubscriptions = Map.insert eid subIds (commentSubscriptions s) }

UnsubscribeToCommentsFor eid -> do
pool <- get @RelayPool
let subIds = Map.findWithDefault [] eid (commentSubscriptions pool)

forM_ subIds stopSubscription

modify @RelayPool $ \s ->
s { commentSubscriptions = Map.delete eid (commentSubscriptions pool) }

-- | Wait until at least one relay is connected
awaitAtLeastOneConnected' :: InboxModelEff es => Eff es Bool
awaitAtLeastOneConnected' = do
Expand Down Expand Up @@ -298,7 +324,7 @@ continueWithRelays inboxRelays = do
then do
subscribeToProfilesAndPosts relayUri pubkeys
else
logError $ "Failed to connect to Follow Relay: " <> relayUri
logError $ "Failed to connect to relay: " <> relayUri

-- | Subscribe to Giftwrap events on a relay
subscribeToGiftwraps :: InboxModelEff es => RelayURI -> PubKeyXO -> Eff es ()
Expand Down
37 changes: 18 additions & 19 deletions src/Nostr/Types.hs
Original file line number Diff line number Diff line change
Expand Up @@ -190,27 +190,26 @@ data Kind
deriving (Eq, Generic, Read, Show)


kindToInt :: Kind -> Int
kindToInt = \case
Metadata -> 0
ShortTextNote -> 1
FollowList -> 3
EventDeletion -> 5
Repost -> 6
Reaction -> 7
GenericRepost -> 16
Seal -> 13
GiftWrap -> 1059
DirectMessage -> 14
PreferredDMRelays -> 10050
CanonicalAuthentication -> 22242
RelayListMetadata -> 10002
Comment -> 1111
UnknownKind n -> n

instance Ord Kind where
compare k1 k2 = compare (kindToInt k1) (kindToInt k2)
where
kindToInt :: Kind -> Int
kindToInt = \case
Metadata -> 0
ShortTextNote -> 1
FollowList -> 3
EventDeletion -> 5
Repost -> 6
Reaction -> 7
GenericRepost -> 16
Seal -> 13
GiftWrap -> 1059
DirectMessage -> 14
PreferredDMRelays -> 10050
CanonicalAuthentication -> 22242
RelayListMetadata -> 10002
Comment -> 1111
UnknownKind n -> n


-- | Represents an event id as a byte string.
newtype EventId = EventId { getEventId :: ByteString } deriving (Eq, Ord)
Expand Down
4 changes: 3 additions & 1 deletion src/QtQuick.hs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import Effectful.TH
import Graphics.QML qualified as QML

import Logging
import Nostr.Types (EventId)
import Types (AppState(..))


Expand All @@ -32,6 +33,7 @@ data UIReferences = UIReferences
{ profileObjRef :: Maybe (QML.ObjRef ())
, followsObjRef :: Maybe (QML.ObjRef ())
, postsObjRef :: Maybe (QML.ObjRef ())
, currentPostCommentsObjRef :: Maybe (QML.ObjRef EventId)
, privateMessagesObjRef :: Maybe (QML.ObjRef ())
, dmRelaysObjRef :: Maybe (QML.ObjRef ())
, generalRelaysObjRef :: Maybe (QML.ObjRef ())
Expand Down Expand Up @@ -86,7 +88,7 @@ initialQtQuickState = QtQuickState Nothing Nothing initialUIRefs Nothing

-- | Initial UI references.
initialUIRefs :: UIReferences
initialUIRefs = UIReferences Nothing Nothing Nothing Nothing Nothing Nothing Nothing
initialUIRefs = UIReferences Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing


-- | Define the effects for QML operations.
Expand Down
Loading
Loading