Rework how hiding messages works. (#1509)

* Rework how hiding messages work. Fixes #1350

* Remove unused function

* Revert to old event name to support previously saved webhooks
This commit is contained in:
Gabe Kangas 2021-11-02 18:00:15 -07:00 committed by GitHub
parent 2278fec70a
commit b43c5e674e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 93 additions and 86 deletions

View File

@ -10,8 +10,8 @@ const (
UserJoined EventType = "USER_JOINED" UserJoined EventType = "USER_JOINED"
// UserNameChanged is the event sent when a chat username change takes place. // UserNameChanged is the event sent when a chat username change takes place.
UserNameChanged EventType = "NAME_CHANGE" UserNameChanged EventType = "NAME_CHANGE"
// VisibiltyToggled is the event sent when a chat message's visibility changes. // VisibiltyUpdate is the event sent when a chat message's visibility changes.
VisibiltyToggled EventType = "VISIBILITY-UPDATE" VisibiltyUpdate EventType = "VISIBILITY-UPDATE"
// PING is a ping message. // PING is a ping message.
PING EventType = "PING" PING EventType = "PING"
// PONG is a pong message. // PONG is a pong message.

View File

@ -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,
}
}

View File

@ -1,6 +1,8 @@
package chat package chat
import ( import (
"errors"
"github.com/owncast/owncast/core/chat/events" "github.com/owncast/owncast/core/chat/events"
"github.com/owncast/owncast/core/webhooks" "github.com/owncast/owncast/core/webhooks"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
@ -14,23 +16,26 @@ func SetMessagesVisibility(messageIDs []string, visibility bool) error {
return err return err
} }
// Send an update event to all clients for each message. // Send an event letting the chat clients know to hide or show
// Note: Our client expects a single message at a time, so we can't just // the messages.
// send an array of messages in a single update. event := events.SetMessageVisibilityEvent{
for _, id := range messageIDs { MessageIDs: messageIDs,
message, err := getMessageByID(id) Visible: visibility,
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)
} }
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 return nil
} }

View File

@ -161,7 +161,7 @@ func GetChatModerationHistory() []events.UserMessageEvent {
} }
// Get all messages regardless of visibility // 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) result := getChat(query)
_historyCache = &result _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. // GetChatHistory will return all the chat messages suitable for returning as user-facing chat history.
func GetChatHistory() []events.UserMessageEvent { func GetChatHistory() []events.UserMessageEvent {
// Get all visible messages // 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) m := getChat(query)
// Invert order of messages // 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) + ")") stmt, err := tx.Prepare("UPDATE messages SET hidden_at=? WHERE id IN (?" + strings.Repeat(",?", len(messageIDs)-1) + ")")
if err != nil { if err != nil {
return err return err
} }
@ -252,41 +251,6 @@ func saveMessageVisibility(messageIDs []string, visible bool) error {
return nil 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, &timestamp)
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 // Only keep recent messages so we don't keep more chat data than needed
// for privacy and efficiency reasons. // for privacy and efficiency reasons.
func runPruner() { func runPruner() {

View File

@ -29,6 +29,7 @@ export default class Chat extends Component {
this.receivedFirstMessages = false; this.receivedFirstMessages = false;
this.receivedMessageUpdate = false; this.receivedMessageUpdate = false;
this.hasFetchedHistory = false; this.hasFetchedHistory = false;
this.forceRender = false;
this.windowBlurred = false; this.windowBlurred = false;
this.numMessagesSinceBlur = 0; this.numMessagesSinceBlur = 0;
@ -69,6 +70,11 @@ export default class Chat extends Component {
const { webSocketConnected, messages, chatUserNames, newMessagesReceived } = const { webSocketConnected, messages, chatUserNames, newMessagesReceived } =
this.state; this.state;
if (this.forceRender) {
return true;
}
const { const {
webSocketConnected: nextSocket, webSocketConnected: nextSocket,
messages: nextMessages, messages: nextMessages,
@ -185,42 +191,51 @@ export default class Chat extends Component {
(item) => item.id === messageId (item) => item.id === messageId
); );
// If the message already exists and this is an update event const updatedMessageList = [...curMessages];
// then update it.
// Change the visibility of messages by ID.
if (messageType === 'VISIBILITY-UPDATE') { 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 = { const convertedMessage = {
...message, ...message,
type: 'CHAT', type: 'CHAT',
}; };
// if message exists and should now hide, take it out.
if (existingIndex >= 0 && !messageVisible) { // insert message at timestamp
this.setState({ const insertAtIndex = curMessages.findIndex((item, index) => {
messages: curMessages.filter((item) => item.id !== messageId), const time = item.timestamp || messageTimestamp;
}); const nextMessage =
} else if (existingIndex === -1 && messageVisible) { index < curMessages.length - 1 && curMessages[index + 1];
// insert message at timestamp const nextTime = nextMessage.timestamp || messageTimestamp;
const insertAtIndex = curMessages.findIndex((item, index) => { const messageTimestampDate = new Date(messageTimestamp);
const time = item.timestamp || messageTimestamp; return (
const nextMessage = messageTimestampDate > new Date(time) &&
index < curMessages.length - 1 && curMessages[index + 1]; messageTimestampDate <= new Date(nextTime)
const nextTime = nextMessage.timestamp || messageTimestamp; );
const messageTimestampDate = new Date(messageTimestamp); });
return ( updatedMessageList.splice(insertAtIndex + 1, 0, convertedMessage);
messageTimestampDate > new Date(time) && if (updatedMessageList.length > 300) {
messageTimestampDate <= new Date(nextTime) updatedMessageList = updatedMessageList.slice(
); Math.max(updatedMessageList.length - 300, 0)
}); );
updatedMessageList.splice(insertAtIndex + 1, 0, convertedMessage);
if (updatedMessageList.length > 300) {
updatedMessageList = updatedMessageList.slice(
Math.max(updatedMessageList.length - 300, 0)
);
}
this.setState({
messages: updatedMessageList,
});
} }
this.setState({
messages: updatedMessageList,
});
} else if (existingIndex === -1) { } else if (existingIndex === -1) {
// else if message doesn't exist, add it and extra username // else if message doesn't exist, add it and extra username
const newState = { const newState = {
@ -354,6 +369,8 @@ export default class Chat extends Component {
const { username, readonly, chatInputEnabled, inputMaxBytes } = props; const { username, readonly, chatInputEnabled, inputMaxBytes } = props;
const { messages, chatUserNames, webSocketConnected } = state; const { messages, chatUserNames, webSocketConnected } = state;
this.forceRender = false;
const messageList = messages const messageList = messages
.filter((message) => message.visible !== false) .filter((message) => message.visible !== false)
.map( .map(