Skip to content

Commit

Permalink
Use whispering for typing indicators
Browse files Browse the repository at this point in the history
  • Loading branch information
palkan committed Apr 2, 2024
1 parent e81aa59 commit fa4a915
Show file tree
Hide file tree
Showing 6 changed files with 69 additions and 4 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/system_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ jobs:
ANYCABLE_HOST: "0.0.0.0"
ANYCABLE_REDIS_URL: redis://host.docker.internal:6379/0
ANYCABLE_RPC_HOST: host.docker.internal:50051
ANYCABLE_STREAMS_SECRET: "secr3t"
steps:
- uses: actions/checkout@v3
- name: Install PostgreSQL client
Expand Down Expand Up @@ -98,6 +99,7 @@ jobs:
ANYCABLE_REDIS_URL: redis://host.docker.internal:6379/0
ANYCABLE_RPC_HOST: host.docker.internal:50051
ANYCABLE_DEBUG: "true"
ANYCABLE_STREAMS_SECRET: "secr3t"
steps:
- uses: actions/checkout@v3
- name: Install PostgreSQL client
Expand Down
2 changes: 1 addition & 1 deletion app/channels/chat_channel.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ def subscribed
self.workspace = Workspace.find_by(public_id: params[:id])
return reject unless workspace

stream_for workspace
stream_for workspace, whisper: true
end

def speak(data)
Expand Down
3 changes: 2 additions & 1 deletion app/views/chats/_chat.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
<p class="text-gray-300 w-full text-center my-4" data-target="chat.placeholder">You will receive new messages as they come</p>
</div>
<div class="sticky bottom-0 inset-auto left-0 right-0 bg-white py-4 bg-opacity-90">
<form data-action="chat#send" class="flex items-center border-b-2 border-teal-500 mb-4 pb-1">
<div data-target="chat.typings" class="text-xs text-gray-400 text-right mb-2 absolute top-0 right-0"></div>
<form data-action="chat#send" class="flex items-center border-b-2 border-teal-500 mb-4 pb-1 mt-2">
<input autocomplete="off" data-target="chat.input" name="message" class="any-text-input" type="text" placeholder="Type message">
<button type="submit" class="flex-shrink-0 any-button">Send</button>
</form>
Expand Down
60 changes: 58 additions & 2 deletions frontend/controllers/chat_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,15 @@ import { Controller } from "@hotwired/stimulus";
import { createCable } from "../utils/cable";
import { currentUser } from "../utils/current_user";
import { isPreview as isTurboPreview } from "../utils/turbo";
import { debounce } from "@github/mini-throttle";

export default class extends Controller {
static targets = ["input", "messages", "placeholder"];
static targets = ["input", "messages", "placeholder", "typings"];

initialize() {
// Store typing user names
this.typings = {};
}

connect() {
if (isTurboPreview()) return;
Expand All @@ -14,23 +20,51 @@ export default class extends Controller {

this.channel = cable.subscribeTo("ChatChannel", { id });
this.channel.on("message", (data) => this.handleMessage(data));

this.deboucedHandleInput = debounce(this.handleInput.bind(this), 300, {
start: true,
});
this.inputTarget.addEventListener("input", this.deboucedHandleInput);
}

disconnect() {
if (this.channel) {
this.channel.disconnect();
delete this.channel;
}

this.inputTarget.removeEventListener("input", this.deboucedHandleInput);
}

handleMessage(data) {
if (data.action == "newMessage") {
if (data.action === "newMessage") {
this.hidePlaceholder();
const mine = currentUser().id == data.author_id;
this.appendMessage(data.html, mine);
delete this.typings[data.author_id];
this.invalidateTypings();
}

if (data.action === "typing" && data.id != currentUser().id) {
this.typings[data.id] = { name: data.name, timestamp: Date.now() };
this.invalidateTypings();
}
}

handleInput(_e) {
const message = this.inputTarget.value.trim();
if (!message) return;

const name = currentUser().name;
if (!name) return;

const id = currentUser().id;

if (!this.channel) return;

this.channel.whisper({ action: "typing", name, id });
}

hidePlaceholder() {
if (this.placeholderTarget.classList.contains("hidden")) return;

Expand Down Expand Up @@ -62,4 +96,26 @@ export default class extends Controller {

this.channel.perform("speak", { message });
}

invalidateTypings() {
let names = [];
for (const id in this.typings) {
const typing = this.typings[id];
if (Date.now() - typing.timestamp > 3000) {
delete this.typings[id];
} else {
names.push(typing.name);
}
}

if (names.length === 0) {
this.typingsTarget.innerText = "";
} else if (names.length === 1) {
this.typingsTarget.innerText = `${names[0]} is typing...`;
} else {
this.typingsTarget.innerText = `${names.length} persons are typing...`;
}

setTimeout(() => this.invalidateTypings(), 1000);
}
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"dependencies": {
"@anycable/core": "^0.8.1",
"@anycable/web": "^0.8.0",
"@github/mini-throttle": "2.1.0",
"@hotwired/stimulus": "^3.2.1",
"@hotwired/turbo-rails": "^7.2.4",
"@rails/actioncable": "^7.0.4",
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,11 @@
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.17.5.tgz#a8f3d26d8afc5186eccda265ceb1820b8e8830be"
integrity sha512-XgA9qWRqby7xdYXuF6KALsn37QGBMHsdhmnpjfZtYxKxbTOwfnDM6MYi2WuUku5poNaX2n9XGVr20zgT/2QwCw==

"@github/[email protected]":
version "2.1.0"
resolved "https://registry.yarnpkg.com/@github/mini-throttle/-/mini-throttle-2.1.0.tgz#583a1d5e383caa21a1c067a649f15f7ab575dccf"
integrity sha512-iEeR2OdVCPkdIPUszL8gJnKNu4MR8ANh7y0u/LPyaatYezgaWxUZEzhFntloqQq+HE6MZkFy+cl+xzCNuljOdw==

"@hotwired/stimulus@^3.2.1":
version "3.2.1"
resolved "https://registry.yarnpkg.com/@hotwired/stimulus/-/stimulus-3.2.1.tgz#e3de23623b0c52c247aba4cd5d530d257008676b"
Expand Down

0 comments on commit fa4a915

Please sign in to comment.