Skip to content

Commit

Permalink
message handling and UI improvements
Browse files Browse the repository at this point in the history
- Fix duplicate messages when sending to self by using nub to deduplicate recipients
- Rename sendMessage to sendPrivateMessage for clarity
- Fix repost/quote handling to properly reference original event
- Add proper message sorting by timestamp in chat threads
- Fix UI layout issues in PostContent component
- Add visibility control for delete button based on ownership
- Fix opacity handling in main content area
  • Loading branch information
prolic committed Dec 2, 2024
1 parent a247f9a commit 8f5d498
Show file tree
Hide file tree
Showing 7 changed files with 69 additions and 42 deletions.
1 change: 0 additions & 1 deletion resources/qml/content/Components/MessageInput.ui.qml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ Pane {

property string placeholderText: qsTr("Type a message...")
property string buttonText: qsTr("Send")
property alias textArea: messageInput

signal messageSent(string text)

Expand Down
14 changes: 9 additions & 5 deletions resources/qml/content/Components/PostContent.ui.qml
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,19 @@ Pane {
Rectangle {
visible: post.postType === "repost" || post.postType === "quote_repost"
Layout.fillWidth: true
color: Qt.rgba(0, 0, 0, 0.1) // Blackish background
color: Qt.rgba(0, 0, 0, 0.1)
radius: 8
border.width: 1
border.color: Material.dividerColor
height: contentColumn.height + 2 * Constants.spacing_m

implicitHeight: contentColumn.implicitHeight + 2 * Constants.spacing_m

ColumnLayout {
id: contentColumn
width: parent.width - 2 * Constants.spacing_m
x: Constants.spacing_m
y: Constants.spacing_m
anchors {
fill: parent
margins: Constants.spacing_m
}
spacing: Constants.spacing_s

// Author info row
Expand Down Expand Up @@ -143,6 +145,8 @@ Pane {
implicitHeight: 36
padding: 8
icon.color: Material.secondaryTextColor
visible: mynpub == npub

onClicked: deleteDialog.open()
}

Expand Down
19 changes: 14 additions & 5 deletions resources/qml/content/MainContent.ui.qml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ Rectangle {
isQuoteMode: true

onMessageSubmitted: function(text) {
quoteRepost(targetPost.id, text)
if (targetPost.postType == "repost") {
quoteRepost(targetPost.referencedEventId, text)
} else {
quoteRepost(targetPost.id, text)
}
}
}

Expand All @@ -48,7 +52,11 @@ Rectangle {
MenuItem {
text: qsTr("Repost")
onTriggered: {
repost(repostMenu.targetPost.id)
if (repostMenu.targetPost.postType == "repost") {
repost(repostMenu.targetPost.referencedEventId, text)
} else {
repost(repostMenu.targetPost.id, text)
}
}
}

Expand All @@ -62,6 +70,7 @@ Rectangle {
}

ColumnLayout {
id: mainContentArea
anchors.fill: parent
anchors.margins: Constants.spacing_xs
spacing: Constants.spacing_m
Expand Down Expand Up @@ -369,7 +378,7 @@ Rectangle {
placeholderText: qsTr("Type a message...")
buttonText: qsTr("Send")
onMessageSent: function(text) {
sendMessage(text)
sendPrivateMessage(text)
}
}
}
Expand Down Expand Up @@ -421,15 +430,15 @@ Rectangle {
State {
name: "loading"
PropertyChanges { target: loadingIndicator; visible: true }
PropertyChanges { target: contentArea; opacity: 0.5 }
PropertyChanges { target: mainContentArea; opacity: 0.5 }
},
State {
name: "error"
PropertyChanges { target: errorBanner; visible: true }
},
State {
name: "ready"
PropertyChanges { target: contentArea; opacity: 1.0 }
PropertyChanges { target: mainContentArea; opacity: 1.0 }
}
]

Expand Down
8 changes: 5 additions & 3 deletions src/Futr.hs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ module Futr where

import Control.Monad (forM, forM_, unless, void, when)
import Data.Aeson (ToJSON, pairs, toEncoding, (.=))
import Data.List (nub)
import Data.Map.Strict qualified as Map
import Data.Maybe (catMaybes, fromMaybe, listToMaybe)
import Data.Proxy (Proxy(..))
Expand Down Expand Up @@ -72,7 +73,7 @@ data Futr :: Effect where
FollowProfile :: Text -> Futr m ()
UnfollowProfile :: Text -> Futr m ()
OpenChat :: PubKeyXO -> Futr m ()
SendMessage :: Text -> Futr m ()
SendPrivateMessage :: Text -> Futr m ()
SendShortTextNote :: Text -> Futr m ()
Logout :: ObjRef () -> Futr m ()
Repost :: EventId -> Futr m ()
Expand Down Expand Up @@ -182,13 +183,13 @@ runFutr = interpret $ \_ -> \case
modify $ \st' -> st' { currentContact = (Just pubKeyXO, Nothing) }
notify $ emptyUpdates { privateMessagesChanged = True }

SendMessage input -> do
SendPrivateMessage input -> do
st <- get @AppState
case (keyPair st, currentContact st) of
(Just kp, (Just recipient, _)) -> do
now <- getCurrentTime
let senderPubKeyXO = keyPairToPubKeyXO kp
allRecipients = senderPubKeyXO : recipient : []
allRecipients = nub [senderPubKeyXO, recipient]
rumor = createRumor senderPubKeyXO now (map (\xo -> PTag xo Nothing Nothing) [recipient]) input

giftWraps <- forM allRecipients $ \recipient' -> do
Expand Down Expand Up @@ -444,6 +445,7 @@ getPostContent post = do
st <- get @AppState
return $ Map.lookup (postId post) (events st) >>= Just . content . fst


-- | Get the creation timestamp of a post.
getPostCreatedAt :: FutrEff es => Post -> Eff es (Maybe Int)
getPostCreatedAt post = do
Expand Down
31 changes: 14 additions & 17 deletions src/Nostr/GiftWrap.hs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import Effectful
import Effectful.Dispatch.Dynamic (interpret)
import Effectful.State.Static.Shared (State, get, modify)
import Effectful.TH (makeEffect)
import Data.List (sort)
import Data.List (sort, sortBy)
import Data.Map.Strict qualified as Map
import Data.Maybe (mapMaybe)
import Data.Ord (comparing)

import Logging
import Nostr
Expand Down Expand Up @@ -117,19 +118,15 @@ createChatMessage originalEvent decryptedRumor senderPubKey currentTimestamp =

-- | Merge a new chat message into the existing chat map
mergeMessageIntoChats :: PubKeyXO -> ChatMessage -> Map.Map PubKeyXO [ChatMessage] -> Map.Map PubKeyXO [ChatMessage]
mergeMessageIntoChats chatKey chatMsg = Map.alter (addOrUpdateChatThread chatMsg) chatKey


-- | Add a new message to an existing chat thread or create a new thread
addOrUpdateChatThread :: ChatMessage -> Maybe [ChatMessage] -> Maybe [ChatMessage]
addOrUpdateChatThread chatMsg = \case
Just msgs -> Just $ insertUniqueMessage chatMsg msgs
Nothing -> Just [chatMsg]

-- | Insert a message into a list, ensuring no duplicates
insertUniqueMessage :: ChatMessage -> [ChatMessage] -> [ChatMessage]
insertUniqueMessage newMsg = foldr insertIfUnique [newMsg]
where
insertIfUnique msg acc
| chatMessageId msg == chatMessageId newMsg = acc
| otherwise = msg : acc
mergeMessageIntoChats chatKey chatMsg = Map.alter (addMessageIfUnique chatMsg) chatKey


-- | Add a message to a chat thread only if it doesn't already exist
-- Messages are sorted by creation time, newest last
addMessageIfUnique :: ChatMessage -> Maybe [ChatMessage] -> Maybe [ChatMessage]
addMessageIfUnique chatMsg = \case
Nothing -> Just [chatMsg]
Just msgs ->
if any (\msg -> chatMessageId msg == chatMessageId chatMsg) msgs
then Just msgs
else Just $ sortBy (comparing chatMessageCreatedAt) (chatMsg : msgs)
23 changes: 16 additions & 7 deletions src/Nostr/Subscription.hs
Original file line number Diff line number Diff line change
Expand Up @@ -113,13 +113,22 @@ handleEvent' event' r = do

case kind event' of
ShortTextNote -> do
let note = Types.Post
{ postId = eventId event'
, postType = Types.ShortTextNote
, postCreatedAt = createdAt event'
}
-- Check for q-tag to identify quote reposts
let qTags = [t | t@(QTag _ _ _) <- tags event']
let note = case qTags of
(QTag quotedId _ _:_) ->
Types.Post
{ postId = eventId event'
, postType = Types.QuoteRepost quotedId
, postCreatedAt = createdAt event'
}
_ ->
Types.Post
{ postId = eventId event'
, postType = Types.ShortTextNote
, postCreatedAt = createdAt event'
}

-- Check if this post already exists for this pubkey
st <- get @AppState
let existingPosts = Map.findWithDefault [] (pubKey event') (posts st)
alreadyExists = any (\p -> postId p == eventId event') existingPosts
Expand Down Expand Up @@ -149,7 +158,7 @@ handleEvent' event' r = do
st { events = Map.insertWith
(\_ (oldEvent, oldRelays) ->
(oldEvent, maybe oldRelays (:oldRelays) mRelay))
(eventId originalEvent)
eid
(originalEvent, maybe [] (:[]) mRelay)
(events st)
}
Expand Down
15 changes: 11 additions & 4 deletions src/UI.hs
Original file line number Diff line number Diff line change
Expand Up @@ -208,9 +208,12 @@ runUI = interpret $ \_ -> \case
let notes = Map.findWithDefault [] recipient (posts st)
case find (\msg -> postId msg == eid) notes of
Just msg -> case postType msg of
Repost ref -> runE $ getReferencedContent ref
QuoteRepost ref -> runE $ getReferencedContent ref
Comment{rootScope=ref} -> runE $ getReferencedContent ref
Repost ref -> do
content <- runE $ getReferencedContent ref
return $ Just content
QuoteRepost ref -> do
content <- runE $ getReferencedContent ref
return $ Just content
_ -> return Nothing
Nothing -> return Nothing,

Expand Down Expand Up @@ -291,6 +294,10 @@ runUI = interpret $ \_ -> \case
postsPool <- newFactoryPool (newObject postClass)

chatClass <- newClass [
defPropertySigRO' "id" changeKey' $ \obj -> do
let eid = fromObjRef obj :: EventId
return $ pack $ show eid,

defPropertySigRO' "content" changeKey' $ \obj -> do
st <- runE $ get @AppState
let eid = fromObjRef obj :: EventId
Expand Down Expand Up @@ -432,7 +439,7 @@ runUI = interpret $ \_ -> \case
let pubKeyXO = maybe (error "Invalid bech32 public key") id $ bech32ToPubKeyXO npubText
openChat pubKeyXO,

defMethod' "sendMessage" $ \_ input -> runE $ sendMessage input, -- NIP-17 private direct message
defMethod' "sendPrivateMessage" $ \_ input -> runE $ sendPrivateMessage input, -- NIP-17 private direct message

defMethod' "sendShortTextNote" $ \_ input -> runE $ sendShortTextNote input, -- NIP-01 short text note

Expand Down

0 comments on commit 8f5d498

Please sign in to comment.