init commit,

This commit is contained in:
louiscklaw
2025-05-28 09:55:51 +08:00
commit efe70ceb69
8042 changed files with 951668 additions and 0 deletions

View File

@@ -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;

View File

@@ -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%;
}

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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;
}