diff --git a/core/chat/events/eventtype.go b/core/chat/events/eventtype.go index 1b06b6513..459f9d8fb 100644 --- a/core/chat/events/eventtype.go +++ b/core/chat/events/eventtype.go @@ -10,8 +10,8 @@ const ( UserJoined EventType = "USER_JOINED" // UserNameChanged is the event sent when a chat username change takes place. UserNameChanged EventType = "NAME_CHANGE" - // VisibiltyToggled is the event sent when a chat message's visibility changes. - VisibiltyToggled EventType = "VISIBILITY-UPDATE" + // VisibiltyUpdate is the event sent when a chat message's visibility changes. + VisibiltyUpdate EventType = "VISIBILITY-UPDATE" // PING is a ping message. PING EventType = "PING" // PONG is a pong message. diff --git a/core/chat/events/setMessageVisibilityEvent.go b/core/chat/events/setMessageVisibilityEvent.go new file mode 100644 index 000000000..965b98361 --- /dev/null +++ b/core/chat/events/setMessageVisibilityEvent.go @@ -0,0 +1,21 @@ +package events + +// SetMessageVisibilityEvent is the event fired when one or more message +// visibilities are changed. +type SetMessageVisibilityEvent struct { + Event + UserMessageEvent + MessageIDs []string + Visible bool +} + +// GetBroadcastPayload will return the object to send to all chat users. +func (e *SetMessageVisibilityEvent) GetBroadcastPayload() EventPayload { + return EventPayload{ + "type": VisibiltyUpdate, + "id": e.ID, + "timestamp": e.Timestamp, + "ids": e.MessageIDs, + "visible": e.Visible, + } +} diff --git a/core/chat/messages.go b/core/chat/messages.go index e55a28767..33f302be2 100644 --- a/core/chat/messages.go +++ b/core/chat/messages.go @@ -1,6 +1,8 @@ package chat import ( + "errors" + "github.com/owncast/owncast/core/chat/events" "github.com/owncast/owncast/core/webhooks" log "github.com/sirupsen/logrus" @@ -14,23 +16,26 @@ func SetMessagesVisibility(messageIDs []string, visibility bool) error { return err } - // Send an update event to all clients for each message. - // Note: Our client expects a single message at a time, so we can't just - // send an array of messages in a single update. - for _, id := range messageIDs { - message, err := getMessageByID(id) - if err != nil { - log.Errorln(err) - continue - } - payload := message.GetBroadcastPayload() - payload["type"] = events.VisibiltyToggled - if err := _server.Broadcast(payload); err != nil { - log.Debugln(err) - } - - go webhooks.SendChatEvent(message) + // Send an event letting the chat clients know to hide or show + // the messages. + event := events.SetMessageVisibilityEvent{ + MessageIDs: messageIDs, + Visible: visibility, } + event.Event.SetDefaults() + + payload := event.GetBroadcastPayload() + if err := _server.Broadcast(payload); err != nil { + return errors.New("error broadcasting message visibility payload " + err.Error()) + } + + // Send webhook + wh := webhooks.WebhookEvent{ + EventData: event, + Type: event.GetMessageType(), + } + + webhooks.SendEventToWebhooks(wh) return nil } diff --git a/core/chat/persistence.go b/core/chat/persistence.go index 082ffc4f8..fad9ce43f 100644 --- a/core/chat/persistence.go +++ b/core/chat/persistence.go @@ -161,7 +161,7 @@ func GetChatModerationHistory() []events.UserMessageEvent { } // Get all messages regardless of visibility - var query = "SELECT messages.id, user_id, body, eventType, hidden_at, timestamp, display_name, display_color, created_at, disabled_at, previous_names, namechanged_at FROM messages INNER JOIN users ON messages.user_id = users.id ORDER BY timestamp DESC" + query := "SELECT messages.id, user_id, body, eventType, hidden_at, timestamp, display_name, display_color, created_at, disabled_at, previous_names, namechanged_at FROM messages INNER JOIN users ON messages.user_id = users.id ORDER BY timestamp DESC" result := getChat(query) _historyCache = &result @@ -172,7 +172,7 @@ func GetChatModerationHistory() []events.UserMessageEvent { // GetChatHistory will return all the chat messages suitable for returning as user-facing chat history. func GetChatHistory() []events.UserMessageEvent { // Get all visible messages - var query = fmt.Sprintf("SELECT messages.id, user_id, body, eventType, hidden_at, timestamp, display_name, display_color, created_at, disabled_at, previous_names, namechanged_at FROM messages, users WHERE messages.user_id = users.id AND hidden_at IS NULL AND disabled_at IS NULL ORDER BY timestamp DESC LIMIT %d", maxBacklogNumber) + query := fmt.Sprintf("SELECT messages.id, user_id, body, eventType, hidden_at, timestamp, display_name, display_color, created_at, disabled_at, previous_names, namechanged_at FROM messages, users WHERE messages.user_id = users.id AND hidden_at IS NULL AND disabled_at IS NULL ORDER BY timestamp DESC LIMIT %d", maxBacklogNumber) m := getChat(query) // Invert order of messages @@ -221,7 +221,6 @@ func saveMessageVisibility(messageIDs []string, visible bool) error { } stmt, err := tx.Prepare("UPDATE messages SET hidden_at=? WHERE id IN (?" + strings.Repeat(",?", len(messageIDs)-1) + ")") - if err != nil { return err } @@ -252,41 +251,6 @@ func saveMessageVisibility(messageIDs []string, visible bool) error { return nil } -func getMessageByID(messageID string) (*events.UserMessageEvent, error) { - var query = "SELECT * FROM messages WHERE id = ?" - row := _datastore.DB.QueryRow(query, messageID) - - var id string - var userID string - var body string - var eventType models.EventType - var hiddenAt *time.Time - var timestamp time.Time - - err := row.Scan(&id, &userID, &body, &eventType, &hiddenAt, ×tamp) - if err != nil { - log.Errorln(err) - return nil, err - } - - user := user.GetUserByID(userID) - - return &events.UserMessageEvent{ - events.Event{ - Type: eventType, - ID: id, - Timestamp: timestamp, - }, - events.UserEvent{ - User: user, - HiddenAt: hiddenAt, - }, - events.MessageEvent{ - Body: body, - }, - }, nil -} - // Only keep recent messages so we don't keep more chat data than needed // for privacy and efficiency reasons. func runPruner() { diff --git a/webroot/js/components/chat/chat.js b/webroot/js/components/chat/chat.js index e8452306a..97df624a1 100644 --- a/webroot/js/components/chat/chat.js +++ b/webroot/js/components/chat/chat.js @@ -29,6 +29,7 @@ export default class Chat extends Component { this.receivedFirstMessages = false; this.receivedMessageUpdate = false; this.hasFetchedHistory = false; + this.forceRender = false; this.windowBlurred = false; this.numMessagesSinceBlur = 0; @@ -69,6 +70,11 @@ export default class Chat extends Component { const { webSocketConnected, messages, chatUserNames, newMessagesReceived } = this.state; + + if (this.forceRender) { + return true; + } + const { webSocketConnected: nextSocket, messages: nextMessages, @@ -185,42 +191,51 @@ export default class Chat extends Component { (item) => item.id === messageId ); - // If the message already exists and this is an update event - // then update it. + const updatedMessageList = [...curMessages]; + + // Change the visibility of messages by ID. if (messageType === 'VISIBILITY-UPDATE') { - const updatedMessageList = [...curMessages]; + const idsToUpdate = message.ids; + const visible = message.visible; + + updatedMessageList.forEach((item) => { + if (idsToUpdate.includes(item.id)) { + item.visible = visible; + } + + this.forceRender = true; + this.setState({ + messages: updatedMessageList, + }); + }); + return; + } else if (existingIndex === -1 && messageVisible) { const convertedMessage = { ...message, type: 'CHAT', }; - // if message exists and should now hide, take it out. - if (existingIndex >= 0 && !messageVisible) { - this.setState({ - messages: curMessages.filter((item) => item.id !== messageId), - }); - } else if (existingIndex === -1 && messageVisible) { - // insert message at timestamp - const insertAtIndex = curMessages.findIndex((item, index) => { - const time = item.timestamp || messageTimestamp; - const nextMessage = - index < curMessages.length - 1 && curMessages[index + 1]; - const nextTime = nextMessage.timestamp || messageTimestamp; - const messageTimestampDate = new Date(messageTimestamp); - return ( - messageTimestampDate > new Date(time) && - messageTimestampDate <= new Date(nextTime) - ); - }); - updatedMessageList.splice(insertAtIndex + 1, 0, convertedMessage); - if (updatedMessageList.length > 300) { - updatedMessageList = updatedMessageList.slice( - Math.max(updatedMessageList.length - 300, 0) - ); - } - this.setState({ - messages: updatedMessageList, - }); + + // insert message at timestamp + const insertAtIndex = curMessages.findIndex((item, index) => { + const time = item.timestamp || messageTimestamp; + const nextMessage = + index < curMessages.length - 1 && curMessages[index + 1]; + const nextTime = nextMessage.timestamp || messageTimestamp; + const messageTimestampDate = new Date(messageTimestamp); + return ( + messageTimestampDate > new Date(time) && + messageTimestampDate <= new Date(nextTime) + ); + }); + updatedMessageList.splice(insertAtIndex + 1, 0, convertedMessage); + if (updatedMessageList.length > 300) { + updatedMessageList = updatedMessageList.slice( + Math.max(updatedMessageList.length - 300, 0) + ); } + this.setState({ + messages: updatedMessageList, + }); } else if (existingIndex === -1) { // else if message doesn't exist, add it and extra username const newState = { @@ -354,6 +369,8 @@ export default class Chat extends Component { const { username, readonly, chatInputEnabled, inputMaxBytes } = props; const { messages, chatUserNames, webSocketConnected } = state; + this.forceRender = false; + const messageList = messages .filter((message) => message.visible !== false) .map(