Kick off conflicting usernames + Frontend mobile fixes

* When JWT tokens are used to join the chat and the username conflicts:
  instead of renaming the new user to add a "2" it will disconnect the
  original login (sending a message that they have signed in somewhere
  else and are logged out now)
* When disconnected the text entry box will be greyed out.
* Improvements for the mobile user experience: if you're viewing the
  chat history panel and have unread messages or DMs, a number indicator
  appears on the channels button. It is grey for public channel messages
  or red if any of them are DMs
* Fix the emoji picker drop-down on the first messages of a DM thread
ipad-testing
Noah 2023-07-17 20:38:07 -07:00
parent 6724792ba0
commit 75c7511410
4 changed files with 60 additions and 13 deletions

View File

@ -51,7 +51,25 @@ func (s *Server) OnLogin(sub *Subscriber, msg Message) {
} }
// Ensure the username is unique, or rename it. // Ensure the username is unique, or rename it.
msg.Username = s.UniqueUsername(msg.Username) username, err := s.UniqueUsername(msg.Username)
if err != nil {
// If JWT authentication was used: disconnect the original (conflicting) username.
if claims.Subject == msg.Username {
if other, err := s.GetSubscriber(msg.Username); err == nil {
other.ChatServer("You have been signed out of chat because you logged in from another location.")
other.SendJSON(Message{
Action: ActionKick,
})
s.DeleteSubscriber(other)
}
// They will take over their original username.
username = msg.Username
}
// If JWT auth was not used: UniqueUsername already gave them a uniquely spelled name.
}
msg.Username = username
// Use their username. // Use their username.
sub.Username = msg.Username sub.Username = msg.Username

View File

@ -275,8 +275,8 @@ func (s *Server) IterSubscribers(isLocked ...bool) []*Subscriber {
return result return result
} }
// UniqueUsername ensures a username will be unique or renames it. // UniqueUsername ensures a username will be unique or renames it. If the name is already unique, the error result is nil.
func (s *Server) UniqueUsername(username string) string { func (s *Server) UniqueUsername(username string) (string, error) {
var ( var (
subs = s.IterSubscribers() subs = s.IterSubscribers()
usernames = map[string]interface{}{} usernames = map[string]interface{}{}
@ -297,7 +297,11 @@ func (s *Server) UniqueUsername(username string) string {
} }
} }
return username if username != origUsername {
return username, errors.New("username was not unique and a unique name has been returned")
}
return username, nil
} }
// Broadcast a message to the chat room. // Broadcast a message to the chat room.

View File

@ -975,6 +975,23 @@ const app = Vue.createApp({
} }
return this.channels[channel].unread; return this.channels[channel].unread;
}, },
hasAnyUnread() {
// Returns total unread count (for mobile responsive view to show in the left drawer button)
let count = 0;
for (let channel of Object.keys(this.channels)) {
count += this.channels[channel].unread;
}
return count;
},
anyUnreadDMs() {
// Returns true if any unread messages are DM threads
for (let channel of Object.keys(this.channels)) {
if (channel.indexOf("@") === 0 && this.channels[channel].unread > 0) {
return true;
}
}
return false;
},
openDMs(user) { openDMs(user) {
let channel = "@" + user.username; let channel = "@" + user.username;
this.initHistory(channel); this.initHistory(channel);

View File

@ -503,7 +503,7 @@
<div class="columns is-mobile card-header-title has-text-light"> <div class="columns is-mobile card-header-title has-text-light">
<div class="column is-narrow mobile-only"> <div class="column is-narrow mobile-only">
<button type="button" <button type="button"
class="button is-success" class="button is-success px-2"
@click="openChatPanel"> @click="openChatPanel">
<i class="fa fa-arrow-left"></i> <i class="fa fa-arrow-left"></i>
</button> </button>
@ -525,7 +525,7 @@
:class="{'is-active': c.ID == channel}"> :class="{'is-active': c.ID == channel}">
[[c.Name]] [[c.Name]]
<span v-if="hasUnread(c.ID)" <span v-if="hasUnread(c.ID)"
class="tag is-danger"> class="tag">
[[hasUnread(c.ID)]] [[hasUnread(c.ID)]]
</span> </span>
</a> </a>
@ -581,11 +581,16 @@
<div class="column is-narrow mobile-only pr-0"> <div class="column is-narrow mobile-only pr-0">
<!-- Responsive mobile button to pan to Left Column --> <!-- Responsive mobile button to pan to Left Column -->
<button type="button" <button type="button"
class="button is-success" class="button is-success px-2"
:class="{'is-small': isDM}"
@click="openChannelsPanel"> @click="openChannelsPanel">
<i v-if="isDM" class="fa fa-arrow-left"></i> <i v-if="isDM" class="fa fa-arrow-left"></i>
<i v-else class="fa fa-message"></i> <i v-else class="fa fa-comments"></i>
<!-- Indicator badge for unread messages -->
<span v-if="hasAnyUnread() > 0"
class="tag ml-1" :class="{'is-danger': anyUnreadDMs()}">
[[hasAnyUnread()]]
</span>
</button> </button>
</div> </div>
<div class="column"> <div class="column">
@ -613,7 +618,7 @@
<div v-if="!isDM" class="column is-narrow mobile-only"> <div v-if="!isDM" class="column is-narrow mobile-only">
<!-- Responsive mobile button to pan to Right Column --> <!-- Responsive mobile button to pan to Right Column -->
<button type="button" <button type="button"
class="button is-success" class="button is-success px-2"
@click="openWhoPanel"> @click="openWhoPanel">
<i class="fa fa-user-group"></i> <i class="fa fa-user-group"></i>
</button> </button>
@ -881,7 +886,9 @@
</div> </div>
<!-- Emoji reactions menu --> <!-- Emoji reactions menu -->
<div v-if="msg.msgID" class="dropdown is-up is-right emoji-button" onclick="this.classList.toggle('is-active')"> <div v-if="msg.msgID" class="dropdown is-right emoji-button"
:class="{'is-up': i >= 2}"
onclick="this.classList.toggle('is-active')">
<div class="dropdown-trigger"> <div class="dropdown-trigger">
<button class="button is-small px-2" aria-haspopup="true" :aria-controls="`react-menu-${msg.msgID}`"> <button class="button is-small px-2" aria-haspopup="true" :aria-controls="`react-menu-${msg.msgID}`">
<span> <span>
@ -956,7 +963,8 @@
<input type="text" class="input" <input type="text" class="input"
v-model="message" v-model="message"
placeholder="Message" placeholder="Message"
@keydown="sendTypingNotification()"> @keydown="sendTypingNotification()"
:disabled="!ws.connected">
</form> </form>
</div> </div>
<div class="column pl-1 is-narrow"> <div class="column pl-1 is-narrow">
@ -979,7 +987,7 @@
<div class="column">Who Is Online</div> <div class="column">Who Is Online</div>
<div class="column is-narrow mobile-only"> <div class="column is-narrow mobile-only">
<button type="button" <button type="button"
class="button is-success" class="button is-success px-2"
@click="openChatPanel"> @click="openChatPanel">
<i class="fa fa-arrow-left"></i> <i class="fa fa-arrow-left"></i>
</button> </button>