diff --git a/api/channels-list/index.ts b/api/channels-list/index.ts index 278e21f..4dfcdaf 100644 --- a/api/channels-list/index.ts +++ b/api/channels-list/index.ts @@ -2,13 +2,17 @@ import "../startup"; import { Context, HttpRequest } from "@azure/functions"; import { authorized } from "../common/ApiRequestContext"; -type ChannelSummary = { name: string }; +type ChannelSummary = { name: string; type: string }; type ChannelListResponse = { channels: ChannelSummary[] }; export default async function (context: Context, req: HttpRequest): Promise { await authorized(context, req, () => { const channels: ChannelListResponse = { - channels: [{ name: "global-welcome" }, { name: "some-other-channel" }] + channels: [ + { name: "global-welcome", type: "public" }, + { name: "some-other-channel", type: "public" }, + { name: "utility", type: "private" } + ] }; context.res = { status: 200, body: JSON.stringify(channels) }; diff --git a/app/src/components/Channels/ChannelBrowser.jsx b/app/src/components/Channels/ChannelBrowser.jsx index a1dd1bf..9dcfde4 100644 --- a/app/src/components/Channels/ChannelBrowser.jsx +++ b/app/src/components/Channels/ChannelBrowser.jsx @@ -7,12 +7,16 @@ import ChatContainer from "../Chat/ChatContainer"; export default function ({ toggleChannelView }) { const { api } = useAuth(); const [channels, setChannels] = useState([]); - const [currentChannel, setCurrentChannel] = useState("global-welcome"); + const [currentChannel, setCurrentChannel] = useState(null); + + const [] = useState(""); useEffect(() => { const fetchChannels = async () => { const response = await api.listChannels(); - setChannels(response.channels); + const { channels } = response; + setChannels(channels); + setCurrentChannel(channels[0].name); }; fetchChannels(); }, []); @@ -23,7 +27,7 @@ export default function ({ toggleChannelView }) { toggleChannelView(); }; - return ( + return !currentChannel ? null : ( <> diff --git a/app/src/components/Channels/ChannelList.jsx b/app/src/components/Channels/ChannelList.jsx index b5f4fda..70987b6 100644 --- a/app/src/components/Channels/ChannelList.jsx +++ b/app/src/components/Channels/ChannelList.jsx @@ -10,7 +10,8 @@ const ChannelList = ({ channels, onChannelSelected }) => { onChannelSelected(channel.name); }; - const channelListItems = channels.map((channel) => ( + const publicChannels = channels.filter((el) => el.type === "public"); + const channelListItems = publicChannels.map((channel) => (
  • { + return
    {message}
    ; +}; + +const formatMessage = (eventObject) => { + // coerse String to Object + return typeof eventObject === "string" // + ? { text: `${eventObject || ""}`.trim() } + : eventObject; +}; + const ChatContainer = ({ currentChannel, onChatExit }) => { const endOfChatLog = useRef(null); + const [archive, rewind] = useArchive(currentChannel); + const [history, setHistory] = useState([]); + const [status, setStatus] = useState([]); + const [activity, setActivity] = useState(); - useEffect(() => { - setHistory([]); // Reset history on channel change - }, [currentChannel]); + // Reset history on channel change const [channel] = useChannel(currentChannel, (message) => { setHistory((prev) => [...prev.slice(-199), message]); }); - const [archive, rewind] = useArchive(currentChannel); + const sendMessage = (eventObject = null) => { + channel.publish("message", formatMessage(eventObject)); + setStatusMessage(null); + }; - const sendMessage = (messageText) => { - channel.publish("message", { text: messageText }); + const sendStatus = (eventObject = null) => { + channel.presence.enter(); + channel.presence.update(formatMessage(eventObject)); }; + const handlePresenceUpdate = (member) => { + const { data, clientId, connectionId } = member; + const { text } = data; + + if (text === activity) return; + + // clients on channel excluding the author as shallow copy Object + let clients = []; + + channel.presence.get((_, members) => { + const typing = clientId === member.clientId; + clients = members.map((client) => ({ ...client, typing })); + console.log(typing, clientId, member.clientId); + }); + + switch (text) { + case "start": + console.log(text); + setActivity("Typing ..."); + break; + + case "done": + console.log(text); + setActivity(""); + break; + + default: + break; + } + }; + + useEffect(() => setHistory([]), [currentChannel]); + useEffect(() => setStatus(activity), [activity]); + useEffect(() => channel.presence.subscribe(handlePresenceUpdate)); + autoScrollHistory(archive, endOfChatLog); return ( @@ -42,8 +94,9 @@ const ChatContainer = ({ currentChannel, onChatExit }) => {
  • - - + + + ); }; diff --git a/app/src/components/Chat/ChatInput.jsx b/app/src/components/Chat/ChatInput.jsx index 0c40eb3..5238a6b 100644 --- a/app/src/components/Chat/ChatInput.jsx +++ b/app/src/components/Chat/ChatInput.jsx @@ -1,33 +1,57 @@ import React from "react"; +import ContentEditable from "react-contenteditable"; -export const ChatInput = ({ sendMessage }) => { +let timer = -1; + +export const ChatInput = ({ sendMessage, sendStatus }) => { const [message, setMessage] = React.useState(""); + const clearStatusAfter = 5 * 1000; // miliseconds + + React.useEffect(() => { + if (!message) return; + if (timer < 0) sendStatus("start"); + + const callback = () => { + sendStatus("done"); + console.log({ timer }); + timer = -1; + }; + + clearTimeout(timer); + timer = setTimeout(callback, clearStatusAfter); + }, [message]); const handleSubmit = (e) => { e.preventDefault(); - if (message.trim() === "") { - return; - } + if (!`${message}`.trim()) return; + sendMessage(message); setMessage(""); + clearTimeout(timer); + timer = -1; + sendStatus("done"); + }; + + const handleChange = ({ target }) => setMessage(target.value || ""); + + const handleKeydown = (e) => { + const passive = /^(control|arrow|shift|alt|meta|page|insert|home)/i; + if (passive.test(e.code)) return; + + switch (e.code) { + case "Enter": + handleSubmit(e); + break; + + default: + // tarpit for keypress + break; + } }; return (
    - + ); diff --git a/app/src/components/Chat/chat.css b/app/src/components/Chat/chat.css index ef97d06..0f7e02c 100644 --- a/app/src/components/Chat/chat.css +++ b/app/src/components/Chat/chat.css @@ -7,22 +7,31 @@ .send { display: flex; - margin: 0 1rem 1rem; + flex-direction: row; + margin: 0 0.5rem; + font-size: 1rem; } -.send-input { +.send-status { + color: #777777; + font-size: 0.8rem; + height: 1.5rem; + margin: 0.25rem 0.5rem 0; +} + +.send-input, +.send-message { width: 100%; padding: var(--spacer); - font-size: 1rem; font-family: inherit; + border: 1px solid black; + border-radius: 0; } - .send-button { - padding: 1rem 2rem; + padding: 0.5rem 1rem; border: 0; - font-family: inherit; - font-size: 1rem; font-weight: bold; + font-family: inherit; background-color: var(--primary); color: var(--primary-text); cursor: pointer; diff --git a/package-lock.json b/package-lock.json index 30b9add..e1b0e92 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "ansi-regex": "^5.0.1", "prop-types": "^15.7.2", "react": "^17.0.2", + "react-contenteditable": "^3.3.6", "react-dom": "^17.0.2", "react-router-dom": "^5.3.0", "tslib": "^2.3.1" @@ -1238,12 +1239,6 @@ "node": ">= 6" } }, - "node_modules/@types/auth0": { - "version": "2.34.10", - "resolved": "https://registry.npmjs.org/@types/auth0/-/auth0-2.34.10.tgz", - "integrity": "sha512-MgNPL7hEwWTPPmq9zin10kTTpdGvZLgpz7sBVK3YPyqasDGtuqJs6MiLkroKWhQq4W6lVFUQ52Wuo5QUomuOxA==", - "dev": true - }, "node_modules/@types/babel__core": { "version": "7.1.16", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.16.tgz", @@ -3604,8 +3599,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-diff": { "version": "1.2.0", @@ -6341,6 +6335,18 @@ "node": ">=0.10.0" } }, + "node_modules/react-contenteditable": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/react-contenteditable/-/react-contenteditable-3.3.6.tgz", + "integrity": "sha512-61+Anbmzggel1sP7nwvxq3d2woD3duR5R89RoLGqKan1A+nruFIcmLjw2F+qqk70AyABls0BDKzE1vqS1UIF1g==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "prop-types": "^15.7.1" + }, + "peerDependencies": { + "react": ">=16.3" + } + }, "node_modules/react-dom": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", @@ -8520,12 +8526,6 @@ "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", "dev": true }, - "@types/auth0": { - "version": "2.34.10", - "resolved": "https://registry.npmjs.org/@types/auth0/-/auth0-2.34.10.tgz", - "integrity": "sha512-MgNPL7hEwWTPPmq9zin10kTTpdGvZLgpz7sBVK3YPyqasDGtuqJs6MiLkroKWhQq4W6lVFUQ52Wuo5QUomuOxA==", - "dev": true - }, "@types/babel__core": { "version": "7.1.16", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.16.tgz", @@ -10274,8 +10274,7 @@ "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "fast-diff": { "version": "1.2.0", @@ -12344,6 +12343,15 @@ "object-assign": "^4.1.1" } }, + "react-contenteditable": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/react-contenteditable/-/react-contenteditable-3.3.6.tgz", + "integrity": "sha512-61+Anbmzggel1sP7nwvxq3d2woD3duR5R89RoLGqKan1A+nruFIcmLjw2F+qqk70AyABls0BDKzE1vqS1UIF1g==", + "requires": { + "fast-deep-equal": "^3.1.3", + "prop-types": "^15.7.1" + } + }, "react-dom": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", diff --git a/package.json b/package.json index f755c06..02f72fa 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "ansi-regex": "^5.0.1", "prop-types": "^15.7.2", "react": "^17.0.2", + "react-contenteditable": "^3.3.6", "react-dom": "^17.0.2", "react-router-dom": "^5.3.0", "tslib": "^2.3.1"