mirror of
				https://github.com/owncast/owncast.git
				synced 2025-11-04 05:17:27 +08:00 
			
		
		
		
	Support changing your own name and handling name change events
This commit is contained in:
		@ -86,6 +86,9 @@ func (s *Server) userNameChanged(eventData chatClientEvent) {
 | 
				
			|||||||
	receivedEvent.User = savedUser
 | 
						receivedEvent.User = savedUser
 | 
				
			||||||
	receivedEvent.ClientID = eventData.client.id
 | 
						receivedEvent.ClientID = eventData.client.id
 | 
				
			||||||
	webhooks.SendChatEventUsernameChanged(receivedEvent)
 | 
						webhooks.SendChatEventUsernameChanged(receivedEvent)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Resend the client's user so their username is in sync.
 | 
				
			||||||
 | 
						eventData.client.sendConnectedClientInfo()
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func (s *Server) userMessageSent(eventData chatClientEvent) {
 | 
					func (s *Server) userMessageSent(eventData chatClientEvent) {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,11 +1,11 @@
 | 
				
			|||||||
import { ChatMessage } from '../../interfaces/chat-message.model';
 | 
					/* eslint-disable react/no-danger */
 | 
				
			||||||
 | 
					 | 
				
			||||||
interface Props {
 | 
					interface Props {
 | 
				
			||||||
  // eslint-disable-next-line react/no-unused-prop-types
 | 
					  body: string;
 | 
				
			||||||
  message: ChatMessage;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
					// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
				
			||||||
export default function ChatSystemMessage(props: Props) {
 | 
					export default function ChatActionMessage(props: Props) {
 | 
				
			||||||
  return <div>Component goes here</div>;
 | 
					  const { body } = props;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return <div dangerouslySetInnerHTML={{ __html: body }} />;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -3,10 +3,11 @@ import { Virtuoso } from 'react-virtuoso';
 | 
				
			|||||||
import { useRef } from 'react';
 | 
					import { useRef } from 'react';
 | 
				
			||||||
import { LoadingOutlined } from '@ant-design/icons';
 | 
					import { LoadingOutlined } from '@ant-design/icons';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { MessageType } from '../../../interfaces/socket-events';
 | 
					import { MessageType, NameChangeEvent } from '../../../interfaces/socket-events';
 | 
				
			||||||
import s from './ChatContainer.module.scss';
 | 
					import s from './ChatContainer.module.scss';
 | 
				
			||||||
import { ChatMessage } from '../../../interfaces/chat-message.model';
 | 
					import { ChatMessage } from '../../../interfaces/chat-message.model';
 | 
				
			||||||
import { ChatUserMessage } from '..';
 | 
					import { ChatUserMessage } from '..';
 | 
				
			||||||
 | 
					import ChatActionMessage from '../ChatActionMessage';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface Props {
 | 
					interface Props {
 | 
				
			||||||
  messages: ChatMessage[];
 | 
					  messages: ChatMessage[];
 | 
				
			||||||
@ -19,10 +20,20 @@ export default function ChatContainer(props: Props) {
 | 
				
			|||||||
  const chatContainerRef = useRef(null);
 | 
					  const chatContainerRef = useRef(null);
 | 
				
			||||||
  const spinIcon = <LoadingOutlined style={{ fontSize: '32px' }} spin />;
 | 
					  const spinIcon = <LoadingOutlined style={{ fontSize: '32px' }} spin />;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const getNameChangeViewForMessage = (message: NameChangeEvent) => {
 | 
				
			||||||
 | 
					    const { oldName } = message;
 | 
				
			||||||
 | 
					    const { user } = message;
 | 
				
			||||||
 | 
					    const { displayName } = user;
 | 
				
			||||||
 | 
					    const body = `<strong>${oldName}</strong> is now known as <strong>${displayName}</strong>`;
 | 
				
			||||||
 | 
					    return <ChatActionMessage body={body} />;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const getViewForMessage = message => {
 | 
					  const getViewForMessage = message => {
 | 
				
			||||||
    switch (message.type) {
 | 
					    switch (message.type) {
 | 
				
			||||||
      case MessageType.CHAT:
 | 
					      case MessageType.CHAT:
 | 
				
			||||||
        return <ChatUserMessage message={message} showModeratorMenu={false} />;
 | 
					        return <ChatUserMessage message={message} showModeratorMenu={false} />;
 | 
				
			||||||
 | 
					      case MessageType.NAME_CHANGE:
 | 
				
			||||||
 | 
					        return getNameChangeViewForMessage(message);
 | 
				
			||||||
      default:
 | 
					      default:
 | 
				
			||||||
        return null;
 | 
					        return null;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -1,25 +1,44 @@
 | 
				
			|||||||
 | 
					import { useState } from 'react';
 | 
				
			||||||
import { useRecoilValue } from 'recoil';
 | 
					import { useRecoilValue } from 'recoil';
 | 
				
			||||||
 | 
					import { Input, Button } from 'antd';
 | 
				
			||||||
 | 
					import { MessageType } from '../../interfaces/socket-events';
 | 
				
			||||||
import WebsocketService from '../../services/websocket-service';
 | 
					import WebsocketService from '../../services/websocket-service';
 | 
				
			||||||
// import { setLocalStorage } from '../../utils/helpers';
 | 
					import { websocketServiceAtom, chatDisplayNameAtom } from '../stores/ClientConfigStore';
 | 
				
			||||||
import { websocketServiceAtom } from '../stores/ClientConfigStore';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
 | 
					/* eslint-disable @typescript-eslint/no-unused-vars */
 | 
				
			||||||
interface Props {}
 | 
					interface Props {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function NameChangeModal(props: Props) {
 | 
					export default function NameChangeModal(props: Props) {
 | 
				
			||||||
  const websocketService = useRecoilValue<WebsocketService>(websocketServiceAtom);
 | 
					  const websocketService = useRecoilValue<WebsocketService>(websocketServiceAtom);
 | 
				
			||||||
 | 
					  const chatDisplayName = useRecoilValue<string>(chatDisplayNameAtom);
 | 
				
			||||||
 | 
					  const [newName, setNewName] = useState<any>(chatDisplayName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // const handleNameChange = () => {
 | 
					  const handleNameChange = () => {
 | 
				
			||||||
  //   // Send name change
 | 
					    const nameChange = {
 | 
				
			||||||
  //   const nameChange = {
 | 
					      type: MessageType.NAME_CHANGE,
 | 
				
			||||||
  //     type: SOCKET_MESSAGE_TYPES.NAME_CHANGE,
 | 
					      newName,
 | 
				
			||||||
  //     newName,
 | 
					    };
 | 
				
			||||||
  //   };
 | 
					    websocketService.send(nameChange);
 | 
				
			||||||
  //   websocketService.send(nameChange);
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  //   // Store it locally
 | 
					  const saveEnabled =
 | 
				
			||||||
  //   setLocalStorage(KEY_USERNAME, newName);
 | 
					    newName !== chatDisplayName && newName !== '' && websocketService?.isConnected();
 | 
				
			||||||
  // };
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return <div>Name change modal component goes here</div>;
 | 
					  return (
 | 
				
			||||||
 | 
					    <div>
 | 
				
			||||||
 | 
					      Your chat display name is what people see when you send chat messages. Other information can
 | 
				
			||||||
 | 
					      go here to mention auth, and stuff.
 | 
				
			||||||
 | 
					      <Input
 | 
				
			||||||
 | 
					        value={newName}
 | 
				
			||||||
 | 
					        onChange={e => setNewName(e.target.value)}
 | 
				
			||||||
 | 
					        placeholder="Your chat display name"
 | 
				
			||||||
 | 
					        maxLength={10}
 | 
				
			||||||
 | 
					        showCount
 | 
				
			||||||
 | 
					        defaultValue={chatDisplayName}
 | 
				
			||||||
 | 
					      />
 | 
				
			||||||
 | 
					      <Button disabled={!saveEnabled} onClick={handleNameChange}>
 | 
				
			||||||
 | 
					        Change name
 | 
				
			||||||
 | 
					      </Button>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -12,7 +12,7 @@ import appStateModel, {
 | 
				
			|||||||
  AppStateOptions,
 | 
					  AppStateOptions,
 | 
				
			||||||
  makeEmptyAppState,
 | 
					  makeEmptyAppState,
 | 
				
			||||||
} from './application-state';
 | 
					} from './application-state';
 | 
				
			||||||
import { setLocalStorage, getLocalStorage } from '../../utils/helpers';
 | 
					import { setLocalStorage, getLocalStorage } from '../../utils/localStorage';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  ConnectedClientInfoEvent,
 | 
					  ConnectedClientInfoEvent,
 | 
				
			||||||
  MessageType,
 | 
					  MessageType,
 | 
				
			||||||
@ -23,6 +23,7 @@ import {
 | 
				
			|||||||
import handleChatMessage from './eventhandlers/handleChatMessage';
 | 
					import handleChatMessage from './eventhandlers/handleChatMessage';
 | 
				
			||||||
import handleConnectedClientInfoMessage from './eventhandlers/connected-client-info-handler';
 | 
					import handleConnectedClientInfoMessage from './eventhandlers/connected-client-info-handler';
 | 
				
			||||||
import ServerStatusService from '../../services/status-service';
 | 
					import ServerStatusService from '../../services/status-service';
 | 
				
			||||||
 | 
					import handleNameChangeEvent from './eventhandlers/handleNameChangeEvent';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const SERVER_STATUS_POLL_DURATION = 5000;
 | 
					const SERVER_STATUS_POLL_DURATION = 5000;
 | 
				
			||||||
const ACCESS_TOKEN_KEY = 'accessToken';
 | 
					const ACCESS_TOKEN_KEY = 'accessToken';
 | 
				
			||||||
@ -207,6 +208,9 @@ export function ClientConfigStore() {
 | 
				
			|||||||
      case MessageType.CHAT:
 | 
					      case MessageType.CHAT:
 | 
				
			||||||
        handleChatMessage(message as ChatEvent, chatMessages, setChatMessages);
 | 
					        handleChatMessage(message as ChatEvent, chatMessages, setChatMessages);
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
 | 
					      case MessageType.NAME_CHANGE:
 | 
				
			||||||
 | 
					        handleNameChangeEvent(message as ChatEvent, chatMessages, setChatMessages);
 | 
				
			||||||
 | 
					        break;
 | 
				
			||||||
      default:
 | 
					      default:
 | 
				
			||||||
        console.error('Unknown socket message type: ', message.type);
 | 
					        console.error('Unknown socket message type: ', message.type);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					import { ChatMessage } from '../../../interfaces/chat-message.model';
 | 
				
			||||||
 | 
					import { ChatEvent } from '../../../interfaces/socket-events';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default function handleNameChangeEvent(
 | 
				
			||||||
 | 
					  message: ChatEvent,
 | 
				
			||||||
 | 
					  messages: ChatMessage[],
 | 
				
			||||||
 | 
					  setChatMessages,
 | 
				
			||||||
 | 
					) {
 | 
				
			||||||
 | 
					  const updatedMessages = [...messages, message];
 | 
				
			||||||
 | 
					  setChatMessages(updatedMessages);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -3,7 +3,7 @@ import { useRecoilState } from 'recoil';
 | 
				
			|||||||
import VideoJS from './player';
 | 
					import VideoJS from './player';
 | 
				
			||||||
import ViewerPing from './viewer-ping';
 | 
					import ViewerPing from './viewer-ping';
 | 
				
			||||||
import VideoPoster from './VideoPoster';
 | 
					import VideoPoster from './VideoPoster';
 | 
				
			||||||
import { getLocalStorage, setLocalStorage } from '../../utils/helpers';
 | 
					import { getLocalStorage, setLocalStorage } from '../../utils/localStorage';
 | 
				
			||||||
import { isVideoPlayingAtom } from '../stores/ClientConfigStore';
 | 
					import { isVideoPlayingAtom } from '../stores/ClientConfigStore';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const PLAYER_VOLUME = 'owncast_volume';
 | 
					const PLAYER_VOLUME = 'owncast_volume';
 | 
				
			||||||
 | 
				
			|||||||
@ -31,3 +31,8 @@ export interface ChatEvent extends SocketEvent {
 | 
				
			|||||||
  user: User;
 | 
					  user: User;
 | 
				
			||||||
  body: string;
 | 
					  body: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface NameChangeEvent extends SocketEvent {
 | 
				
			||||||
 | 
					  user: User;
 | 
				
			||||||
 | 
					  oldName: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,3 @@
 | 
				
			|||||||
import { message } from 'antd';
 | 
					 | 
				
			||||||
import { MessageType, SocketEvent } from '../interfaces/socket-events';
 | 
					import { MessageType, SocketEvent } from '../interfaces/socket-events';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface SocketMessage {
 | 
					export interface SocketMessage {
 | 
				
			||||||
@ -76,41 +75,45 @@ export default class WebsocketService {
 | 
				
			|||||||
    // Optimization where multiple events can be sent within a
 | 
					    // Optimization where multiple events can be sent within a
 | 
				
			||||||
    // single websocket message. So split them if needed.
 | 
					    // single websocket message. So split them if needed.
 | 
				
			||||||
    const messages = e.data.split('\n');
 | 
					    const messages = e.data.split('\n');
 | 
				
			||||||
    let message: SocketEvent;
 | 
					    let socketEvent: SocketEvent;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // eslint-disable-next-line no-plusplus
 | 
					    // eslint-disable-next-line no-plusplus
 | 
				
			||||||
    for (let i = 0; i < messages.length; i++) {
 | 
					    for (let i = 0; i < messages.length; i++) {
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        message = JSON.parse(messages[i]);
 | 
					        socketEvent = JSON.parse(messages[i]);
 | 
				
			||||||
        if (this.handleMessage) {
 | 
					        if (this.handleMessage) {
 | 
				
			||||||
          this.handleMessage(message);
 | 
					          this.handleMessage(socketEvent);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      } catch (e) {
 | 
					      } catch (e) {
 | 
				
			||||||
        console.error(e, e.data);
 | 
					        console.error(e, e.data);
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (!message.type) {
 | 
					      if (!socketEvent.type) {
 | 
				
			||||||
        console.error('No type provided', message);
 | 
					        console.error('No type provided', socketEvent);
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // Send PONGs
 | 
					      // Send PONGs
 | 
				
			||||||
      if (message.type === MessageType.PING) {
 | 
					      if (socketEvent.type === MessageType.PING) {
 | 
				
			||||||
        this.sendPong();
 | 
					        this.sendPong();
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  isConnected(): boolean {
 | 
				
			||||||
 | 
					    return this.websocket?.readyState === this.websocket?.OPEN;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Outbound: Other components can pass an object to `send`.
 | 
					  // Outbound: Other components can pass an object to `send`.
 | 
				
			||||||
  send(message: any) {
 | 
					  send(socketEvent: any) {
 | 
				
			||||||
    // Sanity check that what we're sending is a valid type.
 | 
					    // Sanity check that what we're sending is a valid type.
 | 
				
			||||||
    if (!message.type || !MessageType[message.type]) {
 | 
					    if (!socketEvent.type || !MessageType[socketEvent.type]) {
 | 
				
			||||||
      console.warn(`Outbound message: Unknown socket message type: "${message.type}" sent.`);
 | 
					      console.warn(`Outbound message: Unknown socket message type: "${socketEvent.type}" sent.`);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const messageJSON = JSON.stringify(message);
 | 
					    const messageJSON = JSON.stringify(socketEvent);
 | 
				
			||||||
    this.websocket.send(messageJSON);
 | 
					    this.websocket.send(messageJSON);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,6 @@
 | 
				
			|||||||
import React from 'react';
 | 
					import React from 'react';
 | 
				
			||||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
 | 
					import { ComponentStory, ComponentMeta } from '@storybook/react';
 | 
				
			||||||
 | 
					import { RecoilRoot } from 'recoil';
 | 
				
			||||||
import NameChangeModal from '../components/modals/NameChangeModal';
 | 
					import NameChangeModal from '../components/modals/NameChangeModal';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
@ -9,7 +10,11 @@ export default {
 | 
				
			|||||||
} as ComponentMeta<typeof NameChangeModal>;
 | 
					} as ComponentMeta<typeof NameChangeModal>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
					// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
				
			||||||
const Template: ComponentStory<typeof NameChangeModal> = args => <NameChangeModal />;
 | 
					const Template: ComponentStory<typeof NameChangeModal> = args => (
 | 
				
			||||||
 | 
					  <RecoilRoot>
 | 
				
			||||||
 | 
					    <NameChangeModal />
 | 
				
			||||||
 | 
					  </RecoilRoot>
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
					// eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
				
			||||||
export const Basic = Template.bind({});
 | 
					export const Basic = Template.bind({});
 | 
				
			||||||
 | 
				
			|||||||
@ -1,49 +1,3 @@
 | 
				
			|||||||
import { ORIENTATION_LANDSCAPE, ORIENTATION_PORTRAIT } from './constants.js';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function getLocalStorage(key) {
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    return localStorage.getItem(key);
 | 
					 | 
				
			||||||
  } catch (e) {}
 | 
					 | 
				
			||||||
  return null;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function setLocalStorage(key, value) {
 | 
					 | 
				
			||||||
  try {
 | 
					 | 
				
			||||||
    if (value !== '' && value !== null) {
 | 
					 | 
				
			||||||
      localStorage.setItem(key, value);
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      localStorage.removeItem(key);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    return true;
 | 
					 | 
				
			||||||
  } catch (e) {}
 | 
					 | 
				
			||||||
  return false;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function clearLocalStorage(key) {
 | 
					 | 
				
			||||||
  localStorage.removeItem(key);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// jump down to the max height of a div, with a slight delay
 | 
					 | 
				
			||||||
export function jumpToBottom(element, behavior) {
 | 
					 | 
				
			||||||
  if (!element) return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (!behavior) {
 | 
					 | 
				
			||||||
    behavior = document.visibilityState === 'visible' ? 'smooth' : 'instant';
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  setTimeout(
 | 
					 | 
				
			||||||
    () => {
 | 
					 | 
				
			||||||
      element.scrollTo({
 | 
					 | 
				
			||||||
        top: element.scrollHeight,
 | 
					 | 
				
			||||||
        left: 0,
 | 
					 | 
				
			||||||
        behavior: behavior,
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    50,
 | 
					 | 
				
			||||||
    element,
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// convert newlines to <br>s
 | 
					// convert newlines to <br>s
 | 
				
			||||||
export function addNewlines(str) {
 | 
					export function addNewlines(str) {
 | 
				
			||||||
  return str.replace(/(?:\r\n|\r|\n)/g, '<br />');
 | 
					  return str.replace(/(?:\r\n|\r|\n)/g, '<br />');
 | 
				
			||||||
@ -52,9 +6,8 @@ export function addNewlines(str) {
 | 
				
			|||||||
export function pluralize(string, count) {
 | 
					export function pluralize(string, count) {
 | 
				
			||||||
  if (count === 1) {
 | 
					  if (count === 1) {
 | 
				
			||||||
    return string;
 | 
					    return string;
 | 
				
			||||||
  } else {
 | 
					 | 
				
			||||||
    return string + 's';
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					  return `${string}s`;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Trying to determine if browser is mobile/tablet.
 | 
					// Trying to determine if browser is mobile/tablet.
 | 
				
			||||||
@ -66,7 +19,7 @@ export function hasTouchScreen() {
 | 
				
			|||||||
  } else if ('msMaxTouchPoints' in navigator) {
 | 
					  } else if ('msMaxTouchPoints' in navigator) {
 | 
				
			||||||
    hasTouch = navigator.msMaxTouchPoints > 0;
 | 
					    hasTouch = navigator.msMaxTouchPoints > 0;
 | 
				
			||||||
  } else {
 | 
					  } else {
 | 
				
			||||||
    var mQ = window.matchMedia && matchMedia('(pointer:coarse)');
 | 
					    const mQ = window.matchMedia && matchMedia('(pointer:coarse)');
 | 
				
			||||||
    if (mQ && mQ.media === '(pointer:coarse)') {
 | 
					    if (mQ && mQ.media === '(pointer:coarse)') {
 | 
				
			||||||
      hasTouch = !!mQ.matches;
 | 
					      hasTouch = !!mQ.matches;
 | 
				
			||||||
    } else if ('orientation' in window) {
 | 
					    } else if ('orientation' in window) {
 | 
				
			||||||
@ -79,20 +32,6 @@ export function hasTouchScreen() {
 | 
				
			|||||||
  return hasTouch;
 | 
					  return hasTouch;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function getOrientation(forTouch = false) {
 | 
					 | 
				
			||||||
  // chrome mobile gives misleading matchMedia result when keyboard is up
 | 
					 | 
				
			||||||
  if (forTouch && window.screen && window.screen.orientation) {
 | 
					 | 
				
			||||||
    return window.screen.orientation.type.match('portrait')
 | 
					 | 
				
			||||||
      ? ORIENTATION_PORTRAIT
 | 
					 | 
				
			||||||
      : ORIENTATION_LANDSCAPE;
 | 
					 | 
				
			||||||
  } else {
 | 
					 | 
				
			||||||
    // all other cases
 | 
					 | 
				
			||||||
    return window.matchMedia('(orientation: portrait)').matches
 | 
					 | 
				
			||||||
      ? ORIENTATION_PORTRAIT
 | 
					 | 
				
			||||||
      : ORIENTATION_LANDSCAPE;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function padLeft(text, pad, size) {
 | 
					export function padLeft(text, pad, size) {
 | 
				
			||||||
  return String(pad.repeat(size) + text).slice(-size);
 | 
					  return String(pad.repeat(size) + text).slice(-size);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -116,7 +55,7 @@ export function parseSecondsToDurationString(seconds = 0) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function setVHvar() {
 | 
					export function setVHvar() {
 | 
				
			||||||
  var vh = window.innerHeight * 0.01;
 | 
					  const vh = window.innerHeight * 0.01;
 | 
				
			||||||
  // Then we set the value in the --vh custom property to the root of the document
 | 
					  // Then we set the value in the --vh custom property to the root of the document
 | 
				
			||||||
  document.documentElement.style.setProperty('--vh', `${vh}px`);
 | 
					  document.documentElement.style.setProperty('--vh', `${vh}px`);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -129,7 +68,7 @@ export function doesObjectSupportFunction(object, functionName) {
 | 
				
			|||||||
export function classNames(json) {
 | 
					export function classNames(json) {
 | 
				
			||||||
  const classes = [];
 | 
					  const classes = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Object.entries(json).map(function (item) {
 | 
					  Object.entries(json).map(item => {
 | 
				
			||||||
    const [key, value] = item;
 | 
					    const [key, value] = item;
 | 
				
			||||||
    if (value) {
 | 
					    if (value) {
 | 
				
			||||||
      classes.push(key);
 | 
					      classes.push(key);
 | 
				
			||||||
@ -208,7 +147,7 @@ export function paginateArray(items, page, perPage) {
 | 
				
			|||||||
    previousPage: page - 1 ? page - 1 : null,
 | 
					    previousPage: page - 1 ? page - 1 : null,
 | 
				
			||||||
    nextPage: totalPages > page ? page + 1 : null,
 | 
					    nextPage: totalPages > page ? page + 1 : null,
 | 
				
			||||||
    total: items.length,
 | 
					    total: items.length,
 | 
				
			||||||
    totalPages: totalPages,
 | 
					    totalPages,
 | 
				
			||||||
    items: paginatedItems,
 | 
					    items: paginatedItems,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										47
									
								
								web/utils/localStorage.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								web/utils/localStorage.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,47 @@
 | 
				
			|||||||
 | 
					export const LOCAL_STORAGE_KEYS = {
 | 
				
			||||||
 | 
					  username: 'username',
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function getLocalStorage(key) {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    return localStorage.getItem(key);
 | 
				
			||||||
 | 
					  } catch (e) {}
 | 
				
			||||||
 | 
					  return null;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function setLocalStorage(key, value) {
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    if (value !== '' && value !== null) {
 | 
				
			||||||
 | 
					      localStorage.setItem(key, value);
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      localStorage.removeItem(key);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return true;
 | 
				
			||||||
 | 
					  } catch (e) {}
 | 
				
			||||||
 | 
					  return false;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function clearLocalStorage(key) {
 | 
				
			||||||
 | 
					  localStorage.removeItem(key);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// jump down to the max height of a div, with a slight delay
 | 
				
			||||||
 | 
					export function jumpToBottom(element, behavior) {
 | 
				
			||||||
 | 
					  if (!element) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  if (!behavior) {
 | 
				
			||||||
 | 
					    behavior = document.visibilityState === 'visible' ? 'smooth' : 'instant';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  setTimeout(
 | 
				
			||||||
 | 
					    () => {
 | 
				
			||||||
 | 
					      element.scrollTo({
 | 
				
			||||||
 | 
					        top: element.scrollHeight,
 | 
				
			||||||
 | 
					        left: 0,
 | 
				
			||||||
 | 
					        behavior,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    50,
 | 
				
			||||||
 | 
					    element,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user