init commit,
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react';
|
||||
import './Calls.css';
|
||||
|
||||
const Calls = () => {
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonTitle>Calls</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Calls</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Calls;
|
289
99_references/ionic-react-whatsapp-clone-main/src/pages/Chat.css
Normal file
289
99_references/ionic-react-whatsapp-clone-main/src/pages/Chat.css
Normal file
@@ -0,0 +1,289 @@
|
||||
.chat-page ion-header,
|
||||
.chat-page ion-toolbar {
|
||||
|
||||
--min-height: 3.5rem;
|
||||
}
|
||||
|
||||
.chat-page ion-title {
|
||||
|
||||
margin-left: -3.5rem;
|
||||
}
|
||||
|
||||
.chat-page ion-title p {
|
||||
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.chat-contact {
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chat-contact img {
|
||||
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
border-radius: 500px;
|
||||
}
|
||||
|
||||
.chat-contact-details {
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 0.5rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.chat-contact-details p {
|
||||
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.chat-contact-details ion-text {
|
||||
|
||||
font-size: 0.7rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.chat-bubble {
|
||||
|
||||
border-radius: 5px;
|
||||
margin-left: 1rem;
|
||||
margin-right: 1rem;
|
||||
margin-top: 0.8rem;
|
||||
|
||||
padding: 0.5rem;
|
||||
max-width: 80%;
|
||||
clear: both;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
transition: 0.2s all linear;
|
||||
}
|
||||
|
||||
.chat-bubble:last-child {
|
||||
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
|
||||
.bubble-sent {
|
||||
|
||||
background-color: var(--chat-bubble-sent-color);
|
||||
float: right;
|
||||
}
|
||||
|
||||
.bubble-received {
|
||||
|
||||
background-color: var(--chat-bubble-received-color);
|
||||
float: left;
|
||||
}
|
||||
|
||||
.chat-bubble p {
|
||||
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.chat-footer {
|
||||
|
||||
background-color: rgb(22, 22, 22);
|
||||
border-top: 1px solid rgb(47, 47, 47);
|
||||
padding-top: 0.2rem;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.chat-footer ion-textarea {
|
||||
|
||||
background-color: rgb(31, 31, 31);
|
||||
border: 1px solid rgb(36, 36, 36);
|
||||
color: white;
|
||||
border-radius: 25px;
|
||||
padding-left: 0.5rem;
|
||||
caret-color: var(--ion-color-primary);
|
||||
}
|
||||
|
||||
.chat-footer ion-icon {
|
||||
|
||||
font-size: 1.5rem;
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
.chat-input-container {
|
||||
|
||||
width: 70%;
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
|
||||
.chat-send-button {
|
||||
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
position: absolute;
|
||||
right: 17px;
|
||||
margin-top: -0.2rem !important;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.chat-send-button ion-icon {
|
||||
|
||||
color: white;
|
||||
background-color: var(--ion-color-primary);
|
||||
font-size: 1.1rem;
|
||||
border-radius: 500px;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.chat-time {
|
||||
|
||||
color: rgb(165, 165, 165);
|
||||
font-size: 0.75rem;
|
||||
right: 0;
|
||||
bottom: 0 !important;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.bubble-arrow {
|
||||
|
||||
position: absolute;
|
||||
float: left;
|
||||
left: 6px;
|
||||
margin-top: -8px;
|
||||
/* top: 0px; */
|
||||
}
|
||||
|
||||
.bubble-arrow.alt {
|
||||
|
||||
position: relative;
|
||||
bottom: 0px;
|
||||
left: auto;
|
||||
right: -3px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.bubble-arrow:after {
|
||||
|
||||
content: "";
|
||||
position: absolute;
|
||||
border-top: 15px solid var(--chat-bubble-received-color);
|
||||
border-left: 15px solid transparent;
|
||||
border-radius: 4px 0 0 0px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.bubble-arrow.alt:after {
|
||||
|
||||
border-top: 15px solid var(--chat-bubble-sent-color);
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
.chat-reply-to-row {
|
||||
|
||||
bottom: 70px !important;
|
||||
position: absolute;
|
||||
|
||||
border-left: 4px solid rgb(224, 176, 18);
|
||||
width: 100%;
|
||||
background-color: rgb(22, 22, 22);
|
||||
border-top: 1px solid rgb(47, 47, 47);
|
||||
padding: 0.5rem;
|
||||
padding-bottom: 0.8rem;
|
||||
}
|
||||
|
||||
.chat-reply-to-container {
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chat-reply-to-name {
|
||||
|
||||
color: rgb(224, 176, 18);
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.chat-reply-to-message {
|
||||
|
||||
font-size: 0.8rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.all-chats {
|
||||
|
||||
|
||||
}
|
||||
|
||||
.chat-bottom-details {
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
margin-top: 0.4rem;
|
||||
}
|
||||
|
||||
.chat-bottom-details ion-icon {
|
||||
|
||||
font-size: 0.6rem;
|
||||
color: grey;
|
||||
margin-left: 0.5rem;
|
||||
margin-top: 0.05rem;
|
||||
}
|
||||
|
||||
.chat-bottom-details span {
|
||||
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 0.75rem;
|
||||
color: rgb(190, 190, 190);
|
||||
}
|
||||
|
||||
.in-chat-reply-to-container {
|
||||
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
border-left: 3px solid rgb(224, 176, 18);
|
||||
height: fit-content;
|
||||
padding: 0.5rem;
|
||||
border-radius: 5px;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.in-chat-reply-to-container h1 {
|
||||
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: rgb(224, 176, 18);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.in-chat-reply-to-container p {
|
||||
|
||||
color: rgb(167, 167, 167);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.bottom-container {
|
||||
|
||||
position: absolute;
|
||||
bottom: 4.5rem;
|
||||
height: 5rem;
|
||||
background-color: red;
|
||||
width: 100%;
|
||||
}
|
378
99_references/ionic-react-whatsapp-clone-main/src/pages/Chat.js
Normal file
378
99_references/ionic-react-whatsapp-clone-main/src/pages/Chat.js
Normal file
@@ -0,0 +1,378 @@
|
||||
import { IonBackButton, IonButton, IonButtons, IonCol, IonContent, IonFooter, IonGrid, IonHeader, IonIcon, IonPage, IonRow, IonText, IonTextarea, IonTitle, IonToolbar, CreateAnimation, createGesture, useIonViewWillEnter, IonActionSheet, IonToast } from "@ionic/react";
|
||||
import { addOutline, alertOutline, callOutline, cameraOutline, micOutline, send, shareOutline, starOutline, trashOutline, videocamOutline } from "ionicons/icons";
|
||||
import { useRef } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useParams } from "react-router";
|
||||
import { ChatStore, ContactStore } from "../store";
|
||||
import { getNotificationCount, markAllAsRead, sendChatMessage, starChatMessage } from "../store/ChatStore";
|
||||
import { getChat, getChats, getContact } from "../store/Selectors";
|
||||
|
||||
import { useLongPress } from 'react-use';
|
||||
import "./Chat.css"
|
||||
import ReplyTo from "../components/ReplyTo";
|
||||
import { ChatBottomDetails } from "../components/ChatBottomDetails";
|
||||
import { ChatRepliedQuote } from "../components/ChatRepliedQuote";
|
||||
import { useCamera } from "../hooks/useCamera";
|
||||
import { useGallery } from "../hooks/useGallery";
|
||||
|
||||
const Chat = () => {
|
||||
|
||||
const params = useParams();
|
||||
|
||||
// Global State
|
||||
const chat = ChatStore.useState(getChat(params.contact_id));
|
||||
const chats = ChatStore.useState(getChats);
|
||||
const contact = ContactStore.useState(getContact(params.contact_id));
|
||||
const notificationCount = getNotificationCount(chats);
|
||||
|
||||
const { takePhoto } = useCamera();
|
||||
const { prompt } = useGallery();
|
||||
|
||||
// Local state
|
||||
const [ message, setMessage ] = useState("");
|
||||
const [ showSendButton, setShowSendButton ] = useState(false);
|
||||
const [ replyToMessage, setReplyToMessage ] = useState(false);
|
||||
const [ messageSent, setMessageSent ] = useState(false);
|
||||
|
||||
const [ showActionSheet, setShowActionSheet ] = useState(false);
|
||||
const [ actionMessage, setActionMessage ] = useState(false);
|
||||
|
||||
const [ showToast, setShowToast ] = useState(false);
|
||||
const [ toastMessage, setToastMessage ] = useState("");
|
||||
|
||||
// Refs
|
||||
const contentRef = useRef();
|
||||
const swiperRefs = useRef([]);
|
||||
const textareaRef = useRef();
|
||||
const sideRef = useRef();
|
||||
const sendRef = useRef();
|
||||
const replyToAnimationRef = useRef();
|
||||
|
||||
const actionSheetButtons = [
|
||||
|
||||
{
|
||||
text: (actionMessage && actionMessage.starred) ? "Unstar Message" : "Star Message",
|
||||
icon: starOutline,
|
||||
handler: () => starChatMessage(params.contact_id, actionMessage.id)
|
||||
},
|
||||
actionMessage && actionMessage.received ?
|
||||
{
|
||||
text: "Reply To Message",
|
||||
icon: shareOutline,
|
||||
handler: () => showReplyToMessage(actionMessage)
|
||||
}
|
||||
:
|
||||
{
|
||||
text: "Unsend Message",
|
||||
icon: alertOutline,
|
||||
handler: () => toaster("I haven't implemented unsend :) Simple store update though")
|
||||
},
|
||||
{
|
||||
text: "Delete Message",
|
||||
icon: trashOutline,
|
||||
handler: () => toaster("I haven't implemented delete :) Simple store update though"),
|
||||
role: "destructive"
|
||||
}
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
!showActionSheet && setActionMessage(false);
|
||||
}, [ showActionSheet ]);
|
||||
|
||||
// Scroll to end of content
|
||||
// Mark all chats as read if we come into a chat
|
||||
// Set up our swipe events for animations and gestures
|
||||
useIonViewWillEnter(() => {
|
||||
|
||||
scrollToBottom();
|
||||
setupObserver();
|
||||
markAllAsRead(params.contact_id);
|
||||
setSwipeEvents();
|
||||
});
|
||||
|
||||
// For displaying toast messages
|
||||
const toaster = message => {
|
||||
|
||||
setToastMessage(message);
|
||||
setShowToast(true);
|
||||
}
|
||||
|
||||
// Scroll to end of content
|
||||
const scrollToBottom = async () => {
|
||||
|
||||
contentRef.current.scrollToBottom();
|
||||
}
|
||||
|
||||
// Watch for DOM changes
|
||||
// Then scroll to bottom
|
||||
// This ensures that the new chat message has *actually* been rendered
|
||||
// Check this:
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
|
||||
const setupObserver = () => {
|
||||
|
||||
// Mutation Observers watch for DOM changes
|
||||
// This will ensure that we scroll to bottom AFTER the new chat has rendered
|
||||
const observer = new MutationObserver(() => {
|
||||
|
||||
scrollToBottom();
|
||||
});
|
||||
|
||||
// We observe the ion-content (or containing element of chats)
|
||||
observer.observe(contentRef.current, {
|
||||
|
||||
childList: true
|
||||
});
|
||||
}
|
||||
|
||||
// Long press callback
|
||||
const onLongPress = (e) => {
|
||||
|
||||
const elementID = e.target.id;
|
||||
const chatMessageID = elementID.includes("chatText") ? parseInt(elementID.replace("chatText_", "")) : elementID.includes("chatTime") ? parseInt(elementID.replace("chatTime_", "")) : parseInt(elementID.replace("chatBubble_", ""));
|
||||
|
||||
const chatMessage = chat.filter(message => parseInt(message.id) === parseInt(chatMessageID))[0];
|
||||
|
||||
setActionMessage(chatMessage);
|
||||
setShowActionSheet(true);
|
||||
};
|
||||
|
||||
const longPressEvent = useLongPress(onLongPress, {
|
||||
|
||||
isPreventDefault: true,
|
||||
delay: 2000,
|
||||
});
|
||||
|
||||
const showReplyToMessage = async message => {
|
||||
|
||||
// Activate reply-to functionality
|
||||
setReplyToMessage(message);
|
||||
await replyToAnimationRef.current.animation.play();
|
||||
contentRef.current.scrollToBottom(300);
|
||||
}
|
||||
|
||||
const checkBubble = async (bubble, message, event) => {
|
||||
|
||||
if (event.deltaX >= 120) {
|
||||
|
||||
// Activate reply-to functionality
|
||||
bubble.style.transform = "none";
|
||||
showReplyToMessage(message);
|
||||
} else {
|
||||
|
||||
// Put chat bubble back to original position
|
||||
bubble.style.transform = "none";
|
||||
}
|
||||
}
|
||||
|
||||
// Function to move a bubble with the deltaX swipe
|
||||
const moveBubble = (bubble, event) => {
|
||||
|
||||
if (event.velocityX > 0) {
|
||||
|
||||
bubble.style.transform = `translateX(${ event.deltaX }px)`;
|
||||
}
|
||||
}
|
||||
|
||||
const setSwipeEvents = () => {
|
||||
|
||||
chat.forEach((message, index) => {
|
||||
|
||||
if (!message.sent) {
|
||||
|
||||
const chatBubble = swiperRefs.current[index];
|
||||
|
||||
const swipeGesture = createGesture({
|
||||
el: chatBubble,
|
||||
onEnd: e => checkBubble(chatBubble, message, e),
|
||||
onMove: e => moveBubble(chatBubble, e)
|
||||
});
|
||||
|
||||
swipeGesture.enable();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const widthAnimation = {
|
||||
|
||||
property: "width",
|
||||
fromValue: "110%",
|
||||
toValue: "100%"
|
||||
};
|
||||
|
||||
const fadeAnimation = {
|
||||
|
||||
property: "opacity",
|
||||
fromValue: "100%",
|
||||
toValue: "0%"
|
||||
};
|
||||
|
||||
const sideButtonsAnimation = {
|
||||
|
||||
duration: 200,
|
||||
direction: showSendButton ? "normal" : "reverse",
|
||||
iterations: "1",
|
||||
fromTo: [ fadeAnimation ],
|
||||
easing: "ease-in-out"
|
||||
};
|
||||
|
||||
const sendButtonAnimation = {
|
||||
|
||||
duration: showSendButton ? 300 : 100,
|
||||
direction: !showSendButton ? "normal" : "reverse",
|
||||
iterations: "1",
|
||||
fromTo: [ fadeAnimation ],
|
||||
easing: "ease-in-out"
|
||||
};
|
||||
|
||||
const textareaAnimation = {
|
||||
|
||||
duration: 200,
|
||||
direction: !showSendButton ? "normal" : "reverse",
|
||||
iterations: "1",
|
||||
fromTo: [ widthAnimation ],
|
||||
easing: "ease-in-out"
|
||||
};
|
||||
|
||||
// Set the state value when message val changes
|
||||
useEffect(() => {
|
||||
|
||||
setShowSendButton(message !== "");
|
||||
}, [ message ]);
|
||||
|
||||
// Play the animations when the state value changes
|
||||
useEffect(() => {
|
||||
|
||||
textareaRef.current.animation.play();
|
||||
sideRef.current.animation.play();
|
||||
sendRef.current.animation.play();
|
||||
}, [ showSendButton ]);
|
||||
|
||||
const sendMessage = (image = false, imagePath = false) => {
|
||||
|
||||
if (message !== "" || image === true) {
|
||||
|
||||
sendChatMessage(params.contact_id, message, replyToMessage, replyToMessage ? replyToMessage.id : false, image, imagePath);
|
||||
setMessage("");
|
||||
|
||||
setMessageSent(true);
|
||||
setTimeout(() => setMessageSent(false), 10);
|
||||
image && setTimeout(() => scrollToBottom(), 100);
|
||||
}
|
||||
}
|
||||
|
||||
const handlePhoto = async () => {
|
||||
|
||||
const returnedFilePath = await takePhoto();
|
||||
sendMessage(true, returnedFilePath);
|
||||
}
|
||||
|
||||
const handlePrompt = async () => {
|
||||
|
||||
const returnedFilePath = await prompt();
|
||||
sendMessage(true, returnedFilePath);
|
||||
}
|
||||
|
||||
const replyToProps = {
|
||||
|
||||
replyToAnimationRef,
|
||||
replyToMessage,
|
||||
setReplyToMessage,
|
||||
contact: contact.name,
|
||||
messageSent
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
<IonPage className="chat-page">
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonBackButton slot="start" text={ (notificationCount > 0) ? notificationCount : "" } />
|
||||
<IonTitle>
|
||||
|
||||
<div className="chat-contact">
|
||||
<img src={ contact.avatar } alt="avatar" />
|
||||
<div className="chat-contact-details">
|
||||
<p>{ contact.name }</p>
|
||||
<IonText color="medium">last seen today at 22:10</IonText>
|
||||
</div>
|
||||
</div>
|
||||
</IonTitle>
|
||||
|
||||
<IonButtons slot="end">
|
||||
<IonButton fill="clear" onClick={ () => toaster("As this is a UI only, video calling wouldn't work here.")}>
|
||||
<IonIcon icon={ videocamOutline } />
|
||||
</IonButton>
|
||||
|
||||
<IonButton fill="clear" onClick={ () => toaster("As this is a UI only, calling wouldn't work here.")}>
|
||||
<IonIcon icon={ callOutline } />
|
||||
</IonButton>
|
||||
</IonButtons>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonContent id="main-chat-content" ref={ contentRef }>
|
||||
|
||||
{ chat.map((message, index) => {
|
||||
|
||||
const repliedMessage = chat.filter(subMessage => parseInt(subMessage.id) === parseInt(message.replyID))[0];
|
||||
|
||||
return (
|
||||
<div ref={ ref => swiperRefs.current[index] = ref } id={ `chatBubble_${ message.id }`} key={ index } className={ `chat-bubble ${ message.sent ? "bubble-sent" : "bubble-received" }` } { ...longPressEvent }>
|
||||
<div id={ `chatText_${ message.id }`}>
|
||||
|
||||
<ChatRepliedQuote message={ message } contact={ contact } repliedMessage={ repliedMessage } />
|
||||
|
||||
{ message.preview }
|
||||
{ message.image && message.imagePath && <img src={ message.imagePath } alt="chat message" /> }
|
||||
<ChatBottomDetails message={ message } />
|
||||
</div>
|
||||
|
||||
<div className={ `bubble-arrow ${ message.sent && "alt" }` }></div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<IonActionSheet header="Message Actions" subHeader={ actionMessage && actionMessage.preview } isOpen={ showActionSheet } onDidDismiss={ () => setShowActionSheet(false) } buttons={ actionSheetButtons } />
|
||||
|
||||
<IonToast color="primary" isOpen={ showToast } onDidDismiss={ () => setShowToast(false) } message={ toastMessage } position="bottom" duration="3000" />
|
||||
</IonContent>
|
||||
|
||||
{ replyToMessage && <ReplyTo { ...replyToProps } /> }
|
||||
|
||||
<IonFooter className="chat-footer" id="chat-footer">
|
||||
<IonGrid>
|
||||
<IonRow className="ion-align-items-center">
|
||||
<IonCol size="1">
|
||||
<IonIcon icon={ addOutline } color="primary" onClick={ handlePrompt } />
|
||||
</IonCol>
|
||||
|
||||
<div className="chat-input-container">
|
||||
<CreateAnimation ref={ textareaRef } { ...textareaAnimation }>
|
||||
<IonTextarea rows="1" value={ message } onIonChange={ e => setMessage(e.target.value) } />
|
||||
</CreateAnimation>
|
||||
</div>
|
||||
|
||||
<CreateAnimation ref={ sideRef } { ...sideButtonsAnimation }>
|
||||
<IonCol size="1">
|
||||
<IonIcon icon={ cameraOutline } color="primary" onClick={ handlePhoto } />
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="1">
|
||||
<IonIcon icon={ micOutline } color="primary" />
|
||||
</IonCol>
|
||||
</CreateAnimation>
|
||||
|
||||
<CreateAnimation ref={ sendRef } { ...sendButtonAnimation }>
|
||||
<IonCol size="1" className="chat-send-button" onClick={ sendMessage }>
|
||||
<IonIcon icon={ send } />
|
||||
</IonCol>
|
||||
</CreateAnimation>
|
||||
</IonRow>
|
||||
</IonGrid>
|
||||
</IonFooter>
|
||||
</IonPage>
|
||||
);
|
||||
}
|
||||
|
||||
export default Chat;
|
@@ -0,0 +1,112 @@
|
||||
.chat-row {
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
/* justify-content: space-between; */
|
||||
align-items: center;
|
||||
/* align-content: center; */
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
.chat-row ion-item {
|
||||
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chat-row img {
|
||||
|
||||
height: 3rem;
|
||||
width: 3rem;
|
||||
border-radius: 500px;
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
.chat-content {
|
||||
|
||||
padding-bottom: 1rem;
|
||||
padding-top: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.chat-content h2 {
|
||||
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.chat-content p,
|
||||
.chat-content h2 {
|
||||
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.chat-content p {
|
||||
|
||||
font-size: 1rem;
|
||||
margin-top: 0.2rem;
|
||||
color: rgb(153, 153, 153);
|
||||
}
|
||||
|
||||
.chat-content p ion-icon {
|
||||
|
||||
margin-right: 0.4rem;
|
||||
}
|
||||
|
||||
.chat-name-date {
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.chat-details .chat-date {
|
||||
|
||||
color: rgb(153, 153, 153);
|
||||
font-size: 0.8rem;
|
||||
padding-left: 0.5rem;;
|
||||
}
|
||||
|
||||
.chat-details .chat-unread {
|
||||
|
||||
color: var(--ion-color-primary);
|
||||
}
|
||||
|
||||
.chat-notification-count {
|
||||
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.chat-details {
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-content: flex-end;
|
||||
justify-content: flex-end;
|
||||
align-content: flex-end;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.chat-notification {
|
||||
|
||||
font-size: 0.7rem;
|
||||
padding: 0.2rem;
|
||||
background-color: var(--ion-color-primary);
|
||||
border-radius: 500px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
.chat-content-container {
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
@@ -0,0 +1,79 @@
|
||||
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar, IonSearchbar, IonButtons, IonButton, IonIcon, IonItem, IonModal } from '@ionic/react';
|
||||
import { checkmarkDone, createOutline } from 'ionicons/icons';
|
||||
import './Chats.css';
|
||||
|
||||
import { ChatStore, ContactStore } from '../store';
|
||||
import { getContacts, getChats } from '../store/Selectors';
|
||||
import { useEffect, useState } from 'react';
|
||||
import ChatItem from '../components/ChatItem';
|
||||
import { useRef } from 'react';
|
||||
import ContactModal from '../components/ContactModal';
|
||||
|
||||
const Chats = () => {
|
||||
|
||||
const pageRef = useRef();
|
||||
const contacts = ContactStore.useState(getContacts);
|
||||
const latestChats = ChatStore.useState(getChats);
|
||||
|
||||
const [ results, setResults ] = useState(latestChats);
|
||||
const [ showContactModal, setShowContactModal ] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
setResults(latestChats);
|
||||
}, [ latestChats ]);
|
||||
|
||||
const search = e => {
|
||||
|
||||
const searchTerm = e.target.value;
|
||||
|
||||
if (searchTerm !== "") {
|
||||
|
||||
const searchTermLower = searchTerm.toLowerCase();
|
||||
|
||||
const newResults = latestChats.filter(chat => contacts.filter(c => c.id === chat.contact_id)[0].name.toLowerCase().includes(searchTermLower));
|
||||
setResults(newResults);
|
||||
} else {
|
||||
|
||||
setResults(latestChats);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<IonPage ref={ pageRef }>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonButton fill="clear">Edit</IonButton>
|
||||
</IonButtons>
|
||||
<IonButtons slot="end">
|
||||
<IonButton fill="clear" onClick={ () => setShowContactModal(true) }>
|
||||
<IonIcon icon={ createOutline } />
|
||||
</IonButton>
|
||||
</IonButtons>
|
||||
<IonTitle>Chats</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Chats</IonTitle>
|
||||
</IonToolbar>
|
||||
<IonSearchbar onIonChange={ e => search(e) } />
|
||||
</IonHeader>
|
||||
|
||||
|
||||
{ results.map((chat, index) => {
|
||||
|
||||
return <ChatItem chat={ chat } key={ index } />;
|
||||
})}
|
||||
|
||||
<IonModal isOpen={ showContactModal } swipeToClose={ true } presentingElement={ pageRef.current } onDidDismiss={ () => setShowContactModal(false) }>
|
||||
<ContactModal close={ () => setShowContactModal(false) } />
|
||||
</IonModal>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Chats;
|
@@ -0,0 +1,120 @@
|
||||
import { IonCardSubtitle, IonCol, IonContent, IonHeader, IonIcon, IonItem, IonList, IonPage, IonRow, IonText, IonTitle, IonToolbar } from '@ionic/react';
|
||||
import { camera, cloudUpload, cloudUploadOutline, heart, helpOutline, informationOutline, key, laptop, laptopOutline, logoWhatsapp, mailUnreadOutline, notificationsOutline, pencil, qrCodeOutline, star } from 'ionicons/icons';
|
||||
import styles from './Settings.module.scss';
|
||||
|
||||
const Settings = () => {
|
||||
|
||||
const settings = [
|
||||
|
||||
[
|
||||
{
|
||||
title: "Starred Messages",
|
||||
url: "/starred-messages",
|
||||
icon: star,
|
||||
color: "rgb(255, 208, 0)"
|
||||
},
|
||||
{
|
||||
title: "WhatsApp Web/Desktop",
|
||||
icon: laptopOutline,
|
||||
color: "rgb(33, 165, 114)"
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
title: "Account",
|
||||
icon: key,
|
||||
color: "rgb(0, 81, 255)"
|
||||
},
|
||||
{
|
||||
title: "Chats",
|
||||
icon: logoWhatsapp,
|
||||
color: "rgb(79, 182, 96)"
|
||||
},
|
||||
{
|
||||
title: "Notifications",
|
||||
icon: mailUnreadOutline,
|
||||
color: "rgb(233, 46, 46)"
|
||||
},
|
||||
{
|
||||
title: "Storage and Data",
|
||||
icon: cloudUploadOutline,
|
||||
color: "rgb(79, 182, 96)"
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
title: "Help",
|
||||
icon: informationOutline,
|
||||
color: "rgb(0, 81, 255)"
|
||||
},
|
||||
{
|
||||
title: "Tell a Friend",
|
||||
icon: heart,
|
||||
color: "rgb(228, 70, 70)"
|
||||
}
|
||||
]
|
||||
];
|
||||
|
||||
return (
|
||||
<IonPage className={ styles.settingsPage }>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonTitle>Settings</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Settings</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonItem lines="none" className={ `${ styles.statusAvatar }` }>
|
||||
<img src="https://pbs.twimg.com/profile_images/1383061489469292548/5dhsPd4j_400x400.jpg" alt="avatar" />
|
||||
<IonCol className="ion-padding-start">
|
||||
<IonText color="white">
|
||||
<strong>Alan Montgomery</strong>
|
||||
</IonText>
|
||||
<br />
|
||||
<IonText color="medium" className={ styles.smallText }>This is my status!</IonText>
|
||||
</IonCol>
|
||||
|
||||
<IonRow className={ styles.statusActions }>
|
||||
<IonCol size="6">
|
||||
<IonIcon color="primary" icon={ qrCodeOutline } />
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonItem>
|
||||
|
||||
{ settings.map((setting, index) => {
|
||||
|
||||
return (
|
||||
|
||||
<IonList key={ `setting_${ index }` } className={ `${ styles.settingsList } ion-margin-top ion-padding-top` }>
|
||||
|
||||
{ setting.map((option, index) => {
|
||||
|
||||
var itemStyle = { "--setting-item-color": option.color };
|
||||
|
||||
return (
|
||||
|
||||
<IonItem routerLink={ option.url ? option.url : "" } key={ `settingOption_${ index }` } lines="none" detail={ true }>
|
||||
<IonIcon icon={ option.icon } color="white" style={ itemStyle } />
|
||||
<p>{ option.title }</p>
|
||||
</IonItem>
|
||||
);
|
||||
})}
|
||||
</IonList>
|
||||
);
|
||||
})}
|
||||
|
||||
<div className="ion-text-center ion-justify-content-center ion-margin-top ion-padding-top">
|
||||
<IonText>from</IonText>
|
||||
<IonCardSubtitle className="ion-no-padding ion-no-margin">IONIC React HUB</IonCardSubtitle>
|
||||
</div>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Settings;
|
@@ -0,0 +1,69 @@
|
||||
.settingsPage {
|
||||
|
||||
ion-item {
|
||||
|
||||
--background: rgb(27, 27, 27);
|
||||
background: rgb(27, 27, 27);
|
||||
border-top: 1px solid rgb(41, 41, 41);
|
||||
border-bottom: 1px solid rgb(41, 41, 41);
|
||||
padding: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.settingsList {
|
||||
|
||||
ion-item {
|
||||
|
||||
--background: rgb(27, 27, 27);
|
||||
background: rgb(27, 27, 27);
|
||||
// border: none !important;
|
||||
border-top: 1px solid rgb(34, 34, 34);
|
||||
border-bottom: 1px solid rgb(36, 36, 36);
|
||||
padding: 0;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
ion-icon {
|
||||
|
||||
border-radius: 5px;
|
||||
padding: 0.2rem;
|
||||
font-size: 1.4rem;
|
||||
margin-right: 1.2rem;
|
||||
--setting-item-color: white;
|
||||
background-color: var(--setting-item-color);
|
||||
color: rgb(233, 46, 46);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.smallText {
|
||||
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.statusAvatar {
|
||||
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.statusAvatar {
|
||||
|
||||
img {
|
||||
|
||||
height: 3.5rem;
|
||||
width: 3.5rem;
|
||||
border-radius: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
.statusActions {
|
||||
|
||||
ion-icon {
|
||||
|
||||
padding: 0.5rem;
|
||||
background-color: rgb(56, 56, 56);
|
||||
border-radius: 500px;
|
||||
}
|
||||
}
|
@@ -0,0 +1,87 @@
|
||||
import { IonBackButton, IonContent, IonHeader, IonIcon, IonPage, IonTitle, IonToolbar, useIonViewWillEnter } from '@ionic/react';
|
||||
import { chevronForward } from 'ionicons/icons';
|
||||
import { useState } from 'react';
|
||||
import { ChatStore, ContactStore } from '../store';
|
||||
import { getChats, getContacts } from '../store/Selectors';
|
||||
|
||||
import "./Starred.scss";
|
||||
|
||||
const Starred = () => {
|
||||
|
||||
const contacts = ContactStore.useState(getContacts);
|
||||
const chats = ChatStore.useState(getChats);
|
||||
|
||||
const [ starredMessages, setStarredMessages ] = useState(false);
|
||||
|
||||
useIonViewWillEnter(() => {
|
||||
|
||||
var tempChats = [ ...chats ];
|
||||
var starred = [];
|
||||
|
||||
tempChats.forEach(tempChat => {
|
||||
|
||||
tempChat.chats.forEach(chat => {
|
||||
|
||||
if (chat.starred) {
|
||||
|
||||
starred.push({
|
||||
|
||||
contact_id: tempChat.contact_id,
|
||||
...chat
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
setStarredMessages(starred);
|
||||
});
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonBackButton slot="start" text="Settings" />
|
||||
<IonTitle>Starred Messages</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent fullscreen>
|
||||
|
||||
{ starredMessages && starredMessages.map(starredMessage => {
|
||||
|
||||
const { id, contact_id, date, preview, received } = starredMessage;
|
||||
const contact = contacts.filter(c => c.id === contact_id)[0];
|
||||
|
||||
return (
|
||||
<div key={ `${ contact_id }_${ id }` } className="starred-message">
|
||||
<div className="starred-header">
|
||||
|
||||
<div className="starred-contact">
|
||||
<img src={ contact.avatar } alt="starred avatar" />
|
||||
<p>{ contact.name }</p>
|
||||
</div>
|
||||
|
||||
<p className="starred-date">{ date }</p>
|
||||
|
||||
</div>
|
||||
<div className={ `starred-content ${ received ? "received-starred-content" : "sent-starred-content" }` }>
|
||||
<p>{ preview }</p>
|
||||
<IonIcon icon={ chevronForward } />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{ starredMessages.length < 1 &&
|
||||
|
||||
<div className="no-starred">
|
||||
<img src="/assets/nostarred.png" alt="no starred" />
|
||||
<h1>No Starred Messages</h1>
|
||||
<p>Tap and hold on any message to star it, so you can easily find it later.</p>
|
||||
</div>
|
||||
}
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Starred;
|
@@ -0,0 +1,117 @@
|
||||
.starred-message {
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.starred-header {
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.starred-contact {
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
margin-left: 1rem;
|
||||
|
||||
img {
|
||||
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
border-radius: 500px;
|
||||
}
|
||||
|
||||
p {
|
||||
|
||||
margin-left: 1rem;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.starred-date {
|
||||
|
||||
margin-right: 1.5rem;
|
||||
color: rgb(138, 138, 138);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.starred-content {
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-right: 1.5rem;
|
||||
margin-left: 3.2rem;
|
||||
|
||||
ion-icon {
|
||||
|
||||
color: rgb(138, 138, 138);
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
|
||||
border-radius: 10px;
|
||||
max-width: 75%;
|
||||
padding: 0.5rem;
|
||||
margin: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.received-starred-content {
|
||||
|
||||
p {
|
||||
|
||||
background-color: var(--chat-bubble-received-color);
|
||||
}
|
||||
}
|
||||
|
||||
.sent-starred-content {
|
||||
|
||||
p {
|
||||
|
||||
background-color: var(--chat-bubble-sent-color);
|
||||
}
|
||||
}
|
||||
|
||||
.starred-content:not(:first-child) {
|
||||
|
||||
border-bottom: 2px solid rgb(24, 24, 24);
|
||||
}
|
||||
|
||||
.no-starred {
|
||||
|
||||
padding: 3rem;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
margin-top: 3rem;
|
||||
|
||||
img {
|
||||
|
||||
border-radius: 500px;
|
||||
width: 10rem;
|
||||
height: 10rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
|
||||
color: rgb(165, 165, 165);
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
|
||||
color: rgb(165, 165, 165);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
@@ -0,0 +1,53 @@
|
||||
import { IonContent, IonCardTitle, IonIcon, IonCol, IonItem, IonHeader, IonPage, IonTitle, IonToolbar, IonButtons, IonButton, IonText, IonRow } from '@ionic/react';
|
||||
import { add, camera, pencil } from 'ionicons/icons';
|
||||
import styles from './Status.module.scss';
|
||||
|
||||
const Status = () => {
|
||||
return (
|
||||
<IonPage className={ styles.statusPage }>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonButtons slot="start">
|
||||
<IonButton fill="clear">Privacy</IonButton>
|
||||
</IonButtons>
|
||||
<IonTitle>Status</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Status</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonItem lines="none" className={ `${ styles.statusAvatar } ion-margin-top` }>
|
||||
<img src="https://pbs.twimg.com/profile_images/1383061489469292548/5dhsPd4j_400x400.jpg" alt="avatar" />
|
||||
<div className={ styles.imageUpload }>
|
||||
<IonIcon icon={ add } color="white" />
|
||||
</div>
|
||||
<IonCol className="ion-padding-start">
|
||||
<IonText color="white">
|
||||
<strong>My Status</strong>
|
||||
</IonText>
|
||||
<br />
|
||||
<IonText color="medium" className={ styles.smallText }>Add to my status</IonText>
|
||||
</IonCol>
|
||||
|
||||
<IonRow className={ styles.statusActions }>
|
||||
<IonCol size="6">
|
||||
<IonIcon color="primary" icon={ camera } />
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="6">
|
||||
<IonIcon color="primary" icon={ pencil } />
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
</IonItem>
|
||||
|
||||
<p color="medium" className={ `ion-text-center ${ styles.updates }` }>No recent updates to show right now.</p>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
};
|
||||
|
||||
export default Status;
|
@@ -0,0 +1,73 @@
|
||||
.statusPage {
|
||||
|
||||
ion-item {
|
||||
|
||||
--background: rgb(27, 27, 27);
|
||||
background: rgb(27, 27, 27);
|
||||
border-top: 1px solid rgb(41, 41, 41);
|
||||
border-bottom: 1px solid rgb(41, 41, 41);
|
||||
padding: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.updates {
|
||||
|
||||
margin-top: 2rem;
|
||||
text-align: center;
|
||||
background: rgb(27, 27, 27);
|
||||
border-top: 1px solid rgb(41, 41, 41);
|
||||
border-bottom: 1px solid rgb(41, 41, 41);
|
||||
padding: 1rem;
|
||||
color: rgb(144, 144, 144);
|
||||
|
||||
ion-text {
|
||||
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.smallText {
|
||||
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.statusAvatar {
|
||||
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.statusAvatar {
|
||||
|
||||
img {
|
||||
|
||||
height: 3.5rem;
|
||||
width: 3.5rem;
|
||||
border-radius: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
.statusActions {
|
||||
|
||||
ion-icon {
|
||||
|
||||
padding: 0.5rem;
|
||||
background-color: rgb(56, 56, 56);
|
||||
border-radius: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
.imageUpload {
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
background-color: var(--ion-color-primary);
|
||||
border-radius: 500px;
|
||||
height: 1.2rem;
|
||||
width: 1.2rem;
|
||||
margin-left: 2.5rem;
|
||||
margin-top: 1rem;
|
||||
}
|
Reference in New Issue
Block a user