init commit,
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
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;
|
@@ -0,0 +1,252 @@
|
||||
.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%;
|
||||
}
|
@@ -0,0 +1,456 @@
|
||||
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,97 @@
|
||||
.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,94 @@
|
||||
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,161 @@
|
||||
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,58 @@
|
||||
.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,91 @@
|
||||
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,99 @@
|
||||
.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,77 @@
|
||||
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,62 @@
|
||||
.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