initial commit

This commit is contained in:
undefined
2025-01-24 20:05:48 +01:00
commit db55c10f43
484 changed files with 118165 additions and 0 deletions
+141
View File
@@ -0,0 +1,141 @@
import ThreeDotsMenu from "@/components/frontend/ThreeDotsMenu";
import { ARCHIVE_STATUS } from "@/utils/constants";
import moment from "moment";
import React, { useEffect, useState } from "react";
import Skeleton from "react-loading-skeleton";
import MkdSDK from "@/utils/MkdSDK";
import { useSearchParams } from "react-router-dom";
const formatDate = (time) => {
let currentTime = moment(new Date());
let messageDate = moment(time);
if (currentTime.diff(messageDate, "days") > 1) {
return moment(messageDate).format("Do MMM");
} else {
return moment(messageDate).format("hh:mm A");
}
};
export default function ChatTile({ markMessagesAsRead, first, virtual, last, room, rooms, activeRoomId, setActiveRoom, setActiveBooking, setActiveProperty, setMobileChatSection, messages, deleteRoom, archiveRoom, unArchiveRoom }) {
const ctrl = new AbortController();
let sdk = new MkdSDK();
const [photo, setPhoto] = useState()
const [roomUnreadCounter, setRoomUnreadCounter] = useState([])
const [searchParams, setSearchParams] = useSearchParams();
const params = searchParams.get("room_id")
async function getOtherUser() {
await sdk.setTable("user")
const payload = { id: room?.other_user_id }
const result = await sdk.callRestAPI(payload, "GET");
setPhoto(result?.model?.is_photo_approved !== 1 ? null : result?.model?.photo)
return "yes"
}
async function getMessages(room_id) {
const result = await sdk.getChats(room_id);
const unread = result.model.filter((msg) => msg.unread === 1 && msg.chat.user_id !== Number(localStorage.getItem("user"))).map((msg) => msg.id);
markMessagesAsRead(room.id, unread);
return result.model
}
async function getAllMessages(room_id) {
const result = await sdk.getChats(room_id);
const unread = result.model.filter((msg) => msg.unread === 1 && msg.chat.user_id !== Number(localStorage.getItem("user")));
setRoomUnreadCounter(unread)
}
async function markMessagesAsRead(room_id, arr) {
sdk.setTable("chat");
await Promise.all(arr.map((id) => sdk.callRestAPI({ id, unread: 0 }, "PUT")));
setRoomUnreadCounter((prev) => (arr.length > prev ? 0 : prev - arr.length));
}
async function getBookingDetails() {
setActiveProperty({});
setActiveBooking({});
if (room?.booking_id !== null) {
await sdk.setTable("booking")
const payload = { id: room?.booking_id }
const result = await sdk.callRestAPI(payload, "GET");
setActiveBooking(result.model)
setActiveProperty({});
} else {
const user_id = localStorage.getItem("user");
const where = [`ergo_property_spaces.id = ${Number(room?.property_id)} AND ergo_property_spaces.deleted_at IS NULL`];
const result = await sdk.callRawAPI("/v2/api/custom/ergo/popular/PAGINATE", { page: 1, limit: 1, user_id: Number(user_id), where, all: true }, "POST", ctrl.signal);
if (Array.isArray(result.list) && result.list.length > 0) {
setActiveProperty(result.list[0]);
setActiveBooking({})
} else setActiveProperty({})
}
}
useEffect(() => {
getOtherUser()
}, [])
useEffect(() => {
if (room?.id === "temp") return;
getAllMessages(room.id)
}, [room]);
return (
<div
className={`${room.id && activeRoomId == room.id ? "chat-active " : ""} lg:flex w-full justify-between items-center border-b p-3`}
id={`chat-tile-btn-${room.id}`}
>
<div
onClick={() => {
setActiveRoom(room);
setMobileChatSection(true);
getMessages(room.id)
getBookingDetails()
}}
className="flex gap-2 mr-2 cursor-pointer items-center justify-between">
<img
src={photo ?? "/default.png"}
alt=""
className="h-[48px] w-[48px] rounded-full border-2 border-[#D0D5DD] object-cover"
/>
<div className="flex flex-col items-start w-fit">
<h5 className="text-sm font-semibold capitalize">
{first || <Skeleton width={100} />} {last}
</h5>
<p className="text-xs font-light">{" " || <Skeleton width={80} />}</p>
</div>
</div>
<div className="flex gap- items-center justify-end relative">
{room?.id !== "temp" && roomUnreadCounter.length > 0 && <span className="bg-my-gradient h-[20px] w-[20px] rounded-full text-center text-xs leading-[1.7] text-white flex items-center justify-center">{roomUnreadCounter.length}</span>}
<span style={{ fontSize: "10px" }} className="font-light w-[50px] block text-center">{formatDate(room.update_at)}</span>
<div className="block">
<ThreeDotsMenu
direction="vert"
items={[
{
label: "Delete chat",
icon: null,
onClick: () => deleteRoom(room.id),
},
{
label: "Archive chat",
icon: <></>,
onClick: () => archiveRoom(room.id),
notShow: room.is_archive == ARCHIVE_STATUS.IS_ARCHIVE,
},
{
label: "Unarchive chat",
icon: <></>,
onClick: () => unArchiveRoom(room.id),
notShow: room.is_archive == ARCHIVE_STATUS.NOT_ARCHIVE,
},
]}
/>
</div>
</div>
</div>
);
}
@@ -0,0 +1,219 @@
import { LoadingButton } from "@/components/frontend";
import { BOOKING_STATUS } from "@/utils/constants";
import * as yup from "yup";
import { yupResolver } from "@hookform/resolvers/yup";
import React, { useContext } from "react";
import { useForm } from "react-hook-form";
import MkdSDK from "@/utils/MkdSDK";
import { useSearchParams } from "react-router-dom";
import { GlobalContext } from "@/globalContext";
const ImagePreviewModal = ({ activeRoom, getRooms, setMessages, state, setRooms, spaceId, setShowImagePreviewModal, activeBooking }) => {
const [imageSrc, setImageSrc] = React.useState("");
const [messageError, setMessageError] = React.useState("");
const [uploadedFile, setUploadedFile] = React.useState();
const [loading, setLoading] = React.useState(false);
const [searchParams, setSearchParams] = useSearchParams();
const { state: globalState, dispatch: globalDispatch } = useContext(GlobalContext);
const sdk = new MkdSDK();
const schema = yup
.object({
photo: yup.string()
})
.required();
const {
register,
handleSubmit,
setError,
formState: { errors },
} = useForm({
resolver: yupResolver(schema),
});
function handleFilePreview(file){
if (file) {
setUploadedFile(file[0])
setImageSrc(URL.createObjectURL(file[0]));
}
};
async function sendImageMessage() {
if (imageSrc === "") {
setMessageError("Please select an Image");
return;
}
setLoading(true);
const handleImageUpload = async (file) => {
const formData = new FormData();
formData.append("file", file);
try {
const upload = await sdk.uploadImage(formData);
return upload;
} catch (err) {
console.log("err", err);
return "";
}
};
const upload = await handleImageUpload(uploadedFile);
if (upload?.id) {
setLoading(false);
}
try {
let date = new Date().toISOString().split("T")[0];
let date2 = new Date().toISOString().replace("T", " ").split(".")[0];
// if its temporary chat then create new room before message
let result = null;
let is_temp_chat = activeRoom?.is_temp_chat;
let room_id = searchParams.get("room_id");
if (is_temp_chat && room_id === "temp") {
sdk.setTable("room");
result = await sdk.callRestAPI(
{
user_id: state.user,
other_user_id: Number(activeRoom?.other_user_id),
booking_id: activeRoom.booking_id === null ? null : Number(activeRoom?.booking_id),
property_id: spaceId,
user_update_at: date2,
other_user_update_at: date2,
chat_id: -1,
},
"POST",
)
setRooms((prev) => {
const copy = [...prev];
copy[prev.findIndex((rm) => rm?.is_temp_chat)].id = result?.message;
return copy;
})
// setRooms((prev) => [...prev,r])
setActiveRoom((prev) => ({ ...prev, id: result.message }));
}
await sdk.postMessage({
room_id: room_id === "temp" ? result?.message : Number(room_id),
user_id: state.user,
message: upload.url,
date,
other_user_id: activeRoom.other_user_id,
});
let newMessageObj = {
room_id: activeRoom.id,
chat: {
message: upload.url,
user_id: state.user,
// other_user_id: activeRoom.other_user_id,
is_image: true,
timestamp: new Date(),
},
unread: 1,
create_at: new Date().toISOString(),
update_at: new Date().toISOString(),
};
setMessages((prev) => {
const copy = { ...prev };
copy[room_id === "temp" ? result?.message : Number(room_id)] = [...copy[room_id === "temp" ? result?.message : Number(room_id)], newMessageObj];
return copy;
});
// is_temp_chat = false;
setLoading(false);
setShowImagePreviewModal(false);
getRooms()
// send email alert
// sendEmailAlert(activeRoom.other_user_id, activeBooking.property_name, message, activeRoom.id);
} catch (err) {
console.log(err)
globalDispatch({
type: "SHOW_ERROR",
payload: {
heading: "Sending image failed",
message: err.message,
},
});
}
setLoading(false);
}
return (
<>
<div className="fixed inset-0 z-50 overflow-y-auto">
<div
className="fixed inset-0 w-full h-full bg-black opacity-40"
onClick={()=>{setShowImagePreviewModal(false);setImageSrc("")}}
></div>
<div className="flex items-center min-h-screen px-4 mt-4 py-8">
<div className="relative w-full max-w-lg p-4 mx-auto bg-white rounded-md shadow-lg">
<div className="flex mb-4">
<h1 className="text-2xl">Please select an Image</h1>
</div>
<div className="flex mb-4 text-red-600">
<span className="text-xs">{messageError}</span>
</div>
<label
htmlFor="send-picture"
className={`cursor-pointer border p-2.5 rounded-md`}>
<input
className="hidden"
onChange={(e)=> handleFilePreview(e.target.files)}
id="send-picture"
type="file"
accept="image/png, image/gif, image/jpeg"
name="file"
/>
{imageSrc === "" ?
"Select Image"
:
"Update Image"
}
</label>
<div className="mt-3 sm:flex w-full">
<form
onSubmit={handleSubmit(sendImageMessage)}
className="mt-2 text-center sm:text-left w-full">
{imageSrc &&
<img
className="block object-cover w-full h-[200px] md:h-[300px]"
src={imageSrc}
/>
}
<div className="items-center w-full mt-3 flex">
<button
className="flex-1 rounded border border-[#667085] hover:bg-gray-200 !bg-gradient-to-r px-6 py-[10px] text-sm font-semibold text-[#667085] outline-none focus:outline-none"
onClick={()=>{setShowImagePreviewModal(false);setImageSrc("")}}
>
Cancel
</button>
{uploadedFile &&
<LoadingButton
loading={loading}
type="submit"
className={`ml-5 flex-1 block rounded !bg-gradient-to-r from-[#33D4B7] to-[#0D9895] px-4 py-[10px] text-sm font-semibold text-white outline-none focus:outline-none w-[150px] ${loading ? "py-[5px]" : "py[12px]"}`}>
Send
</LoadingButton>
}
</div>
</form>
</div>
</div>
</div>
</div>
</>
);
};
export default ImagePreviewModal;
@@ -0,0 +1,49 @@
import { AuthContext } from "@/authContext";
import moment from "moment";
import React from "react";
import { useContext } from "react";
export default function MessagesContainer({ messages, messageErr }) {
const { state } = useContext(AuthContext);
return (
<div className="">
<div className="flex-grow flex-cols overflow-y-auto tiny-scroll normal-case">
{messages && (
<div className="py-2 relative">
{messages.map((message, idx) => (
<div
key={idx}
className="mb-4 flex"
>
<div className={`flex-1 px-2 ${message?.chat?.user_id === state.user && "text-right"}`}>
<div className="inline-block">
{message?.chat?.message.startsWith("https://s3.us-east-2.amazonaws.com") ? (
<div className={`${message?.chat?.user_id === state.user ? "" : "text-right"} border bg-[#F2F4F7] p-2 rounded-md`}>
<img
src={message?.chat.message}
className="min-h-30 w-[150px] object-cover"
/>
</div>
) : (
<p className={`${message?.chat?.user_id === state.user ? "border-[#F2F4F7] border to-chat" : "bg-[#15212A] text-white from-chat"} block text-start break-all rounded-xl p-2 px-6`}>
{message?.chat?.message}
</p>
)}
</div>
<div className="pl-4 text-[#8E8E93] text-xs">
<small className="text-gray-500">{moment(message?.chat?.timestamp).format("DD-MM, hh:mm A")}</small>
</div>
</div>
</div>
))}
{messageErr && (
<div className="fixed bottom-[6rem] left-0 w-full flex justify-center z-70">
<p className="border text-center border-green-500 bg-green-100 text-green-800 text-sm p-3 rounded-xl">{messageErr}</p>
</div>
)}
</div>
)}
</div>
</div>
);
}
+994
View File
@@ -0,0 +1,994 @@
import React, { useContext, useState, useEffect } from "react";
import MkdSDK from "@/utils/MkdSDK";
import { AuthContext, tokenExpireError } from "@/authContext";
import { Link, useNavigate, useSearchParams } from "react-router-dom";
import SmileIcon from "@/components/frontend/icons/SmileIcon";
import PictureIcon from "@/components/frontend/icons/PictureIcon";
import EmojiPicker from "emoji-picker-react";
import badWords from "./badWords.json";
import * as linkify from "linkifyjs";
import { formatAMPM, monthsMapping } from "@/utils/date-time-utils";
import CircleCheckIcon from "@/components/frontend/icons/CircleCheckIcon";
import { GlobalContext, showToast } from "@/globalContext";
import FavoriteButton from "@/components/frontend/FavoriteButton";
import ChatTile from "./ChatTile";
import MessagesContainer from "./MessagesContainer";
import { ARCHIVE_STATUS, BOOKING_STATUS } from "@/utils/constants";
import { parseJsonSafely } from "@/utils/utils";
import { ArrowLeftIcon, PaperAirplaneIcon } from "@heroicons/react/24/outline";
import TreeSDK from "@/utils/TreeSDK";
import StarIcon from "@/components/frontend/icons/StarIcon";
import PersonIcon from "@/components/frontend/icons/PersonIcon";
import PropertySpaceMapImage from "@/components/frontend/PropertySpaceMapImage";
import ImagePreviewModal from "./ImagePreviewModal";
let sdk = new MkdSDK();
let treeSdk = new TreeSDK();
const ctrl = new AbortController();
const MessagesPage = () => {
const { state, dispatch } = useContext(AuthContext);
const [rooms, setRooms] = useState(Array(4).fill({}));
const [roomUnread, setRoomUnread] = useState([]);
const { state: globalState, dispatch: globalDispatch } = useContext(GlobalContext);
const [message, setMessage] = useState("");
const [searchParams, setSearchParams] = useSearchParams();
const [activeRoom, setActiveRoom] = useState({});
const [activeBooking, setActiveBooking] = useState({});
const [activeProperty, setActiveProperty] = useState({});
const [spaceId, setSpaceId] = useState();
const [messageErr, setMessageErr] = useState("");
const [showMap, setShowMap] = useState(false);
const [virtual, setVirtual] = useState(false);
const [showEmoji, setShowEmoji] = useState(false);
const [unReadCount, setUnreadCount] = useState(globalState.unreadMessages);
const [archivedCount, setArchivedAccount] = useState(0);
const [mobileChatSection, setMobileChatSection] = useState(false);
const [mobilePreviewOpen, setMobilePreviewOpen] = useState(false);
const [favoriteId, setFavoriteId] = useState(null);
const [render, forceRender] = useState(false);
const [fetchingExtra, setFetchingExtra] = useState(true);
const navigate = useNavigate();
const [messages, setMessages] = useState({});
const [sending, setSending] = useState(false);
const [roomsFetched, setRoomsFetched] = useState(false);
const [showImagePreviewModal, setShowImagePreviewModal] = useState(false)
const formatAmenities = (propertyAmenities) => {
var amenities = (propertyAmenities ?? "").split(",");
amenities = Array.from(new Set(amenities));
return amenities
}
const bookingExpired = activeBooking.booking_start_time && activeBooking.status < BOOKING_STATUS.ONGOING ? new Date(activeBooking.booking_end_time) < Date.now() : false;
async function getRooms() {
try {
// const result2 = await treeSdk.getList("room", { join: ["user|other_user_id", "booking"], filter: [`user_id,eq,${state.user}`] });
const result = await sdk.getMyRoom();
if (Array.isArray(result.messages)) {
setUnreadCount(
result.messages.filter((msg) => {
const messageSenderId = JSON.parse(msg.chat).user_id;
return Number(messageSenderId) != Number(state.user);
}).length,
);
globalDispatch({
type: "SET_UNREAD_MESSAGES_COUNT",
payload: result.messages.filter((msg) => {
const messageSenderId = JSON.parse(msg.chat).user_id;
return Number(messageSenderId) != Number(state.user);
}).length,
});
}
setRooms(result?.list);
setRoomUnread(result?.messages)
setRoomsFetched(true);
globalDispatch({ type: "STOP_LOADING" });
} catch (err) {
tokenExpireError(dispatch, err.message);
globalDispatch({
type: "SHOW_ERROR",
payload: {
heading: "Operation failed getting rooms",
message: err.message,
},
});
}
}
async function getArchivedRooms() {
try {
const result = await treeSdk.getList("room", { join: ["user|other_user_id", "booking"], filter: [`user_id,eq,${state.user}`] });
setArchivedAccount((result?.list.filter((item) => item.is_archive == 1)).length)
setRooms(result.list.filter((item) => item.is_archive == 1));
console.log((result?.list.filter((item) => item.is_archive == 1)).length)
globalDispatch({ type: "STOP_LOADING" });
} catch (err) {
tokenExpireError(dispatch, err.message);
globalDispatch({
type: "SHOW_ERROR",
payload: {
heading: "Operation failed getting rooms",
message: err.message,
},
});
}
}
async function getMessages(room_id) {
try {
const result = await sdk.getChats(room_id);
if (Array.isArray(result.model)) {
setMessages((prev) => {
const copy = { ...prev };
copy[room_id] = result.model.sort(sortByUpdateAt);
return copy;
});
}
return result.model
} catch (err) {
tokenExpireError(dispatch, err.message);
globalDispatch({
type: "SHOW_ERROR",
payload: {
heading: "Operation failed getting messages " + room_id,
message: err.message,
},
});
}
}
async function sendMessage() {
if (message == "") return;
setShowEmoji(false);
//Add checks to validate message based on active booking
setSending(true);
try {
let date = new Date().toISOString().split("T")[0];
let date2 = new Date().toISOString().replace("T", " ").split(".")[0];
// if its temporary chat then create new room before message
let result = null;
let is_temp_chat = activeRoom.is_temp_chat;
let room_id = searchParams.get("room_id");
if (is_temp_chat && room_id === "temp") {
sdk.setTable("room");
result = await sdk.callRestAPI(
{
user_id: state.user,
other_user_id: Number(activeRoom.other_user_id),
booking_id: activeRoom.booking_id === null ? null : Number(activeRoom.booking_id),
property_id: spaceId,
user_update_at: date2,
other_user_update_at: date2,
chat_id: -1,
},
"POST",
)
setRooms((prev) => {
const copy = [...prev];
copy[prev.findIndex((rm) => rm?.is_temp_chat)].id = result?.message;
return copy;
})
// setRooms((prev) => [...prev,r])
setActiveRoom((prev) => ({ ...prev, id: result.message }));
}
await sdk.postMessage({
room_id: room_id === "temp" ? result?.message : Number(room_id),
user_id: state.user,
message,
date,
other_user_id: activeRoom.other_user_id,
});
let newMessageObj = {
room_id: activeRoom.id,
chat: {
message: message,
user_id: state.user,
// other_user_id: activeRoom.other_user_id,
is_image: false,
timestamp: new Date(),
},
unread: 1,
create_at: new Date().toISOString(),
update_at: new Date().toISOString(),
};
setMessages((prev) => {
const copy = { ...prev };
copy[room_id === "temp" ? result?.message : Number(room_id)] = [...copy[room_id === "temp" ? result?.message : Number(room_id)], newMessageObj];
return copy;
});
// is_temp_chat = false;
getRooms()
// send email alert
sendEmailAlert(activeRoom.other_user_id, activeBooking.property_name, message, activeRoom.id);
setMessage("");
// TODO: scroll to bottom
} catch (err) {
console.log(err)
globalDispatch({
type: "SHOW_ERROR",
payload: {
heading: "Sending message failed",
message: err.message,
},
});
}
setSending(false);
}
async function sendEmailAlert(to, property_name, message, room_id) {
try {
// get receiver preferences
const result = await sdk.callRawAPI("/v2/api/custom/ergo/get-user", { id: to }, "POST");
if (parseJsonSafely(result.settings, {}).email_on_new_chat_message == true) {
let sender_name = globalState.user.first_name + " " + globalState.user.last_name;
// get email template
const tmpl = await sdk.getEmailTemplate("chat-message-alert");
const body = tmpl.html
?.replace(new RegExp("{{{sender_name}}}", "g"), sender_name)
.replace(new RegExp("{{{property_name}}}", "g"), property_name)
.replace(new RegExp("{{{message}}}", "g"), message)
.replace(new RegExp("{{{room_id}}}", "g"), room_id);
// send email
await sdk.sendEmail(result.email, tmpl.subject, body);
}
} catch (err) {
console.log("ERROR", err);
}
}
async function fetchFavoriteStatus(property_spaces_id, user_id) {
const payload = { property_spaces_id, user_id };
sdk.setTable("user_property_spaces");
try {
const result = await sdk.callRestAPI({ payload }, "GETALL");
if (Array.isArray(result.list) && result.list.length > 0) {
setFavoriteId(result.list[0].id);
} else {
throw new Error("");
}
} catch (err) {
setFavoriteId(null);
}
globalDispatch({ type: "STOP_LOADING" });
}
async function deleteRoom(id) {
sdk.setTable("room");
try {
const result = await sdk.callRestAPI({ id }, "DELETE");
if (!result.error) {
getRooms()
setActiveRoom({})
setActiveBooking({})
setActiveProperty({})
showToast(globalDispatch, result.message, 5000)
}
} catch (err) {
globalDispatch({
type: "SHOW_ERROR",
payload: {
heading: "Operation failed",
message: err.message,
},
});
}
}
async function archiveRoom(id) {
sdk.setTable("room");
// call API - callRestAPI (You can see callRestAPI implementation in other functions) here to archive room chat. Method is PUT
// update archived state of selected room chat without refreshing the page and toast a success message
//Also switch to the archive tab on success of the API, with the archived room chat showing under there
}
async function unArchiveRoom(id) {
sdk.setTable("room");
// call API - callRestAPI (You can see callRestAPI implementation in other functions) here to unarchive room chat. Method is PUT
// update unarchived state of selected room chat without refreshing the page and toast a success message
//Also switch to the inbox tab on success of the API, with the unarchived room chat showing under there
}
async function fetchExtraBookingDetails() {
if (activeBooking?.extrasFetched) return;
setFetchingExtra(true);
try {
const result = await sdk.callRawAPI(`/v2/api/custom/ergo/booking/details`, { where: [`ergo_booking.id = ${activeRoom.booking_id} AND (ergo_booking.deleted_at IS NULL AND ergo_booking.status = ${BOOKING_STATUS.ONGOING} OR ergo_booking.status = ${BOOKING_STATUS.UPCOMING})`] }, "POST");
if (result.list.id && new Date(new Date(result.list.booking_end_time).setDate(new Date(result.list.booking_end_time).getDate() + 1)) > new Date()) {
const fullBooking = {
...result.list,
...activeBooking,
add_ons: result.list.add_ons,
property_name: result.list.property_name,
image: result.list.image_url,
address_line_1: result.list.address_line_1,
address_line_2: result.list.address_line_2,
extrasFetched: true,
};
setRooms((prev) => {
const copy = [...prev];
const pos = copy.findIndex((r) => r.id == activeRoom.id);
if (pos != -1) {
copy[pos].booking = fullBooking;
}
return copy;
});
setActiveRoom((prev) => {
const copy = { ...prev };
copy.booking = fullBooking;
return copy;
});
}
setTimeout(() => {
setFetchingExtra(false);
}, 500);
} catch (err) {
tokenExpireError(dispatch, err.message);
globalDispatch({ type: "SHOW_ERROR", payload: { heading: "Error fetching booking details", message: err.message } });
}
}
async function markMessagesAsRead(room_id, arr) {
try {
sdk.setTable("chat");
await Promise.all(arr.map((id) => sdk.callRestAPI({ id, unread: 0 }, "PUT")));
setMessages((prev) => {
const copy = { ...prev };
copy[room_id] = (copy[room_id] ?? []).map((msg) => ({ ...msg, unread: 0 }));
return copy;
});
setUnreadCount((prev) => {
const newCount = arr.length > prev ? 0 : prev - arr.length;
globalDispatch({
type: "SET_UNREAD_MESSAGES_COUNT",
payload: newCount,
});
return newCount;
});
} catch (err) {
tokenExpireError(dispatch, err.message);
globalDispatch({
type: "SHOW_ERROR",
payload: {
heading: "Error marking messages as read",
message: err.message,
},
});
}
}
async function getPropertyDetails(id) {
const user_id = localStorage.getItem("user");
const where = [`ergo_property_spaces.id = ${id} AND ergo_property_spaces.deleted_at IS NULL`];
try {
const result = await sdk.callRawAPI("/v2/api/custom/ergo/popular/PAGINATE", { page: 1, limit: 1, user_id: Number(user_id), where, all: true }, "POST", ctrl.signal);
if (Array.isArray(result.list) && result.list.length > 0) {
setActiveProperty(result.list[0]);
fetchFavoriteStatus(Number(result.list[0].id), Number(user_id))
} else setActiveProperty({})
} catch (err) {
tokenExpireError(dispatch, err.message);
if (err.name == "AbortError") return;
globalDispatch({
type: "SHOW_ERROR",
payload: {
heading: "Operation failed",
message: err.message,
},
});
}
globalDispatch({ type: "STOP_LOADING" });
}
async function createVirtualRoom(other_user_id, booking_id, new_booking_id) {
setSpaceId(Number(new_booking_id))
try {
const result = await treeSdk.getOne("user", other_user_id, { join: [] });
const room = {
id: "temp",
is_temp_chat: true,
user_id: state.user,
other_user_id,
booking_id,
is_archive: 0,
property_id: new_booking_id,
create_at: new Date(),
update_at: new Date(),
user_update_at: new Date(),
other_user_update_at: new Date(),
deleted_at: null,
user: {
id: other_user_id,
first_name: result.model.deleted_at == null ? result.model.first_name : "[Deleted User]",
last_name: result.model.deleted_at == null ? result.model.last_name : "",
photo: result.model.deleted_at == null ? result.model.photo : null,
},
booking: {
id: booking_id === null ? null : booking_id,
},
};
setRooms((prev) => [...prev, room]);
setVirtual(true)
setActiveRoom(room);
} catch (err) {
tokenExpireError(dispatch, err.message);
globalDispatch({ type: "SHOW_ERROR", payload: { heading: "Failed to fetch other user data", message: err.message } });
}
}
async function getBookingDetails() {
setFetchingExtra(true);
const room_id = searchParams.get("room_id");
const booking_id = searchParams.get("booking");
setActiveProperty({});
setActiveBooking({});
if (room_id === "temp" || room_id === null) return;
sdk.setTable("room")
const data = await sdk.callRestAPI({ id: Number(room_id) }, "GET")
await sdk.setTable("booking")
const bookings = await sdk.callRestAPI({}, "GETALL");
const fetched_booking = bookings.list.reverse().find((item) =>
(item.host_id === data.model?.user_id || item.host_id === data.model?.other_user_id) &&
(item.customer_id === data.model?.user_id || item.customer_id === data.model?.other_user_id) &&
(item.property_space_id === data?.model?.property_id) &&
(item.status === BOOKING_STATUS.UPCOMING || item.status === BOOKING_STATUS.ONGOING)
)
if (data.model?.booking_id !== null || booking_id !== null || (fetched_booking !== undefined && fetched_booking !== null) ) {
if (fetched_booking) {
setActiveBooking(fetched_booking)
return;
}
await sdk.setTable("booking")
const payload = { id: data.model?.booking_id ?? (booking_id ?? fetched_booking?.id) }
const result = await sdk.callRestAPI(payload, "GET");
setActiveBooking((result?.model?.status === BOOKING_STATUS.ONGOING || result?.model?.status === BOOKING_STATUS.UPCOMING) ? result.model : {})
} else {
const user_id = localStorage.getItem("user");
const where = [`ergo_property_spaces.id = ${Number(data.model?.property_id)} AND ergo_property_spaces.deleted_at IS NULL`];
const result = await sdk.callRawAPI("/v2/api/custom/ergo/popular/PAGINATE", { page: 1, limit: 1, user_id: Number(user_id), where, all: true }, "POST", ctrl.signal);
if (Array.isArray(result.list) && result.list.length > 0) {
setActiveProperty(result.list[0]);
setActiveBooking({})
} else setActiveProperty({})
}
setTimeout(() => {
setFetchingExtra(false);
}, 500);
}
useEffect(() => {
getBookingDetails()
}, [searchParams.get("room_id")]);
useEffect(() => {
getRooms();
}, []);
useEffect(() => {
if (!roomsFetched) return;
const room_id = searchParams.get("room_id");
const property_space_id = searchParams.get("space");
setActiveProperty({});
setActiveBooking({});
let room = rooms.find((rm) => rm.id == room_id);
if (room) {
setActiveRoom(room);
return;
}
const other_user_id = searchParams.get("other_user_id");
if (!other_user_id) return;
const booking_id = searchParams.get("booking");
room = rooms.find((rm) => ((rm.booking_id === booking_id && rm.other_user_id === other_user_id) || rm.property_id === Number(property_space_id)));
if (room) {
setActiveRoom(room);
} else {
getPropertyDetails(property_space_id)
createVirtualRoom(other_user_id, booking_id, property_space_id)
}
}, [roomsFetched]);
useEffect(() => {
const controller = new AbortController();
const pollMessages = async () => {
const abortController = new AbortController();
try {
const poll = await sdk.startPolling(localStorage.getItem("user"), abortController.signal)
if (poll.message) {
// Do whatever you want here
let room = searchParams.get("room_id");
if (room === "temp") return;
getRooms()
if (Number(searchParams.get("room_id"))) {
getMessages(Number(searchParams.get("room_id"))).then((res) => {
const unread = res.filter((msg) => msg.unread === 1 && msg.chat.user_id !== state.user).map((msg) => msg.id);
markMessagesAsRead(Number(searchParams.get("room_id")), unread);
}
);
}
}
} catch (error) {
console.log(error)
}
finally {
if (!abortController.signal.aborted) {
pollMessages()
}
}
}
return () => {
clearInterval(5000);
controller.abort();
};
}, []);
function showImageModal(){
setShowImagePreviewModal(true)
}
useEffect(() => {
setFetchingExtra(true);
globalDispatch({ type: "START_LOADING" });
setActiveProperty({});
setActiveBooking({});
if (!activeRoom.id) return;
searchParams.set("room_id", activeRoom.id);
searchParams.set("booking", activeRoom.booking_id);
if (!searchParams.get("booking")) {
searchParams.delete("booking");
}
searchParams.delete("space");
searchParams.delete("other_user_id");
setSearchParams(searchParams);
setMessage("");
getMessages(activeRoom.id).then((res) => {
const unread = res.filter((msg) => msg.unread === 1 && msg.chat.user_id !== state.user).map((msg) => msg.id);
markMessagesAsRead(activeRoom.id, unread);
});
if (activeRoom.booking_id !== null) {
fetchExtraBookingDetails();
}
if (activeRoom.id) {
getPropertyDetails(activeRoom.property_id)
}
setTimeout(() => {
setFetchingExtra(false);
}, 500);
globalDispatch({ type: "STOP_LOADING" });
}, [activeRoom.id, render]);
function sortByUpdateAt(a, b) {
return new Date(a.update_at) - new Date(b.update_at);
}
return (
<>
<div
className="relative -mx-4 flex h-[var(--messages-page-height)] border border-t-0 normal-case md:mx-0"
onClick={() => setShowEmoji(false)}
>
<div className="w-full md:w-[26%]">
<div className="flex h-full flex-col">
<div className="flex border-b border-t nineteen-step">
<button
className={`${searchParams.get("message_tab") != "archive" ? "border-b-2 border-black font-semibold text-black" : ""} flex-grow px-[] py-[12px] text-[]`}
onClick={() => {
searchParams.set("message_tab", "inbox");
setSearchParams(searchParams);
}}
>
Inbox ({unReadCount})
</button>
<button
className={`${searchParams.get("message_tab") == "archive" ? "border-b-2 border-black font-semibold text-black" : ""} flex-grow px-[] py-[12px] text-[]`}
onClick={() => {
searchParams.set("message_tab", "archive");
setSearchParams(searchParams);
}}
>
Archive
</button>
</div>
{roomsFetched &&
<div className="tiny-scroll bg-white md:bg-[#f9fafb] flex-grow overflow-y-auto">
{searchParams.get("message_tab") != "archive" &&
rooms &&
rooms
.filter((rm) => rm.is_archive == ARCHIVE_STATUS.NOT_ARCHIVE)
.sort((a, b) => new Date(b.update_at) - new Date(a.update_at))
.map((room, idx) => {
return (
<ChatTile
key={idx}
room={room}
rooms={rooms}
virtual={virtual}
roomUnread={roomUnread}
activeRoomId={activeRoom.id}
setActiveRoom={setActiveRoom}
setActiveBooking={setActiveBooking}
setActiveProperty={setActiveProperty}
activeBooking={activeBooking}
first={room.first_name ? room.first_name : room.user.first_name}
last={room.last_name ? room.last_name : room.user.last_name}
setMobileChatSection={setMobileChatSection}
markMessagesAsRead={() => markMessagesAsRead}
messages={messages}
deleteRoom={deleteRoom}
archiveRoom={archiveRoom}
unArchiveRoom={unArchiveRoom}
/>
);
})}
{searchParams.get("message_tab") == "archive" &&
rooms &&
rooms
.filter((rm) => rm.is_archive == ARCHIVE_STATUS.IS_ARCHIVE)
.sort((a, b) => new Date(b.update_at) - new Date(a.update_at))
.map((room, idx) => {
return (
<ChatTile
key={idx}
room={room}
roomUnread={roomUnread}
activeRoomId={activeRoom.id}
setActiveRoom={setActiveRoom}
setActiveBooking={setActiveBooking}
setActiveProperty={setActiveProperty}
activeBooking={activeBooking}
first={room.first_name ? room.first_name : room.user.first_name}
last={room.last_name ? room.last_name : room.user.last_name}
setMobileChatSection={setMobileChatSection}
messages={messages}
deleteRoom={deleteRoom}
archiveRoom={archiveRoom}
unArchiveRoom={unArchiveRoom}
/>
);
})}
</div>
}
</div>
</div>
<div className={`${(mobilePreviewOpen && messages[activeRoom.id].length > 0 && !fetchingExtra) ? "block" : "hidden"} absolute top-0 right-0 -left-0 overflow-y-hidden bg-white md:static md:block md:max-h-[unset] md:w-[48%]`}>
<div className="flex h-full flex-col border-t">
{activeRoom?.id ? (
<>
<div className={`${mobileChatSection ? "md:hidden" : "hidden"} pl-2`}>
<button
type="button"
onClick={() => {
setMobileChatSection(false);
setMobilePreviewOpen(false);
}}
className="mr-2 mb-2 inline-flex items-center py-2.5 pr-5 text-center text-sm font-semibold"
>
<ArrowLeftIcon className="h-6 w-6" />
<span className="ml-2">Back</span>
</button>
</div>
<div className="flex justify-between border-b py-[13px] px-2 md:px-4">
<h3 className="md:text-lg text-base font-semibold">Chat with {activeRoom?.first_name === undefined ? rooms[0]?.user?.first_name : activeRoom?.first_name + " " + activeRoom?.last_name === undefined ? rooms[0]?.user?.last_name : activeRoom?.last_name}</h3>
{mobileChatSection && activeRoom.booking_id && (
<button
onClick={() => setMobilePreviewOpen(true)}
className="inline whitespace-nowrap bg-gradient-to-r from-primary to-primary-dark bg-clip-text text-xs font-bold text-transparent md:hidden"
>
Preview booking
</button>
)}
</div>
<div className="h-full w-full overflow-x-hidden relative hidden-scrollbar">
<div className="h-[90%] z-10 pb-12 md:pb-0 overflow-auto">
<MessagesContainer
messageErr={messageErr}
messages={messages[activeRoom.id] ?? []}
/>
</div>
<div className="fixed z- md:absolute bottom-0 w-full overflow-hidden flex h-fit bottom-0 justify-start items-center gap-4 border border-r-0 border-l-0 bg-white px-[20px] py-[12px]">
<div className="flex flex-gro items-center gap-2">
<label
onClick={()=>{([BOOKING_STATUS.PENDING, BOOKING_STATUS.COMPLETED, BOOKING_STATUS.DELETED, BOOKING_STATUS.DECLINED, BOOKING_STATUS.CANCELLED].includes(activeBooking.status) || !activeBooking.status) ? showToast(globalDispatch, "Without a booking, you cant send images or emojis.", 4000, "ERROR") : showImageModal()}}
className={`cursor-pointer ${activeBooking?.status != BOOKING_STATUS.COMPLETED ? "strike-opacity-50 pointer-events-non opacity-50" : ""}`}
>
<PictureIcon />
</label>
<button
onClick={(e) => {
([BOOKING_STATUS.PENDING, BOOKING_STATUS.COMPLETED, BOOKING_STATUS.DELETED, BOOKING_STATUS.DECLINED, BOOKING_STATUS.CANCELLED].includes(activeBooking.status) || !activeBooking.status) ? showToast(globalDispatch, "Without a booking, you cant send images or emojis.", 4000, "ERROR") :
e.stopPropagation();
setShowEmoji(!showEmoji);
}}
className="strike-opacity-50 relative disabled:opacity-50"
>
<SmileIcon />
</button>
</div>
<form
className="relative w-full rounded-md border-[#E5E5EA]"
onSubmit={(e) => {
e.preventDefault();
sendMessage();
}}
>
<input
name="message"
className="border w-full bg-[#F9FAFB] py-1 pl-2 pr-16 text-sm outline-none"
rows="1"
placeholder="Type a message..."
value={message}
onChange={(e) => setMessage(e.target.value)}
autoComplete="off"
/>
<button
type="submit"
className={message ? "absolute right-0 top-0 p-1 px-4 text-gray-900 duration-75 hover:text-primary" : "hidden"}
>
{sending ? (
<svg
className="inline h-4 w-4 animate-spin text-primary"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
) : (
<PaperAirplaneIcon className="h-5 w-5 -rotate-45" />
)}
</button>
</form>
</div>
</div>
</>
) : (
<div className="flex flex-grow items-center justify-center text-4xl text-gray-700 ">Select a chat to view</div>
)}
</div>
</div>
<div className={`${(mobilePreviewOpen && messages[activeRoom.id].length > 0 && !fetchingExtra ) ? "block" : "hidden"} lg:block absolute top-0 right-0 -left-0 overflow-y-auto max-h-[var(--messages-page-height)] bg-white md:static md:block md:max-h-[unset] w-full md:w-[26%]`}>
<div className="flex h-full max-h-[var(--messages-page-height)] flex-col border">
<div className="flex justify-between py-[12px] px-4">
{(activeRoom?.booking_id && activeRoom?.booking?.id) &&
<h3 className="text-lg font-semibold">Booking Preview</h3>
}
{(activeRoom?.booking_id && !activeRoom?.booking?.id) &&
<h3 className="text-lg font-semibold">Property Preview</h3>
}
{mobilePreviewOpen && (
<button
type="button"
onClick={() => setMobilePreviewOpen(false)}
className="inline rounded-full border p-1 px-3 text-2xl font-normal duration-100 hover:bg-gray-200 active:bg-gray-300 md:hidden"
>
&#x2715;
</button>
)}
</div>
<div className="tiny-scroll flex-grow overflow-y-auto">
<div className="min-h-[500px] px-[20px] py-4">
{activeRoom.id && !activeRoom?.booking?.id ? (
<div className="">
<div
className="mb-[8px] rounded-lg bg-cover bg-center bg-no-repeat px-[8px] pb-[13px] relative"
style={{ backgroundImage: `url(${(activeProperty?.url) ?? "/default-property.jpg"})`, height: 150 }}
>
<FavoriteButton
space_id={activeProperty?.id}
user_property_spaces_id={favoriteId || activeProperty?.favourite}
withLoader={true}
reRender={forceRender}
className="flex flex-grow justify-end w-fit float-right pt-2"
/>
<span className="absolute mt-3 px-2 py-1 text-white bg-black font-bold rounded-lg text-xs self-start">{activeProperty?.category || "N/A"}</span>
</div>
<div className="py-6 block justify-between lg:items-start items-end lg:pl-0 w-full">
<div className="">
<h2 className="text-[18px] font-semibold mb-[6px] whitespace-normal md:whitespace-nowrap">{activeProperty?.name}</h2>
<p className="text-[#475467] tracking-wider md:truncate mb-1">{activeProperty?.city}</p>
<p className="text-[#475467] tracking-wider md:truncate">{activeProperty?.country} </p>
<div className="lg:mt-[21px] mt-[6px] flex justify-between">
<p className="mr-[31px]">
from: <span className="font-bold">${activeProperty?.rate}</span>/<span className="">hour</span>
</p>
<div className="flex items-center gap-2">
<PersonIcon />
<span>{activeProperty?.max_capacity}</span>
</div>
</div>
</div>
<div className="grid items-start">
<div className="flex items-center justify-between lg:mb-[9px] mt-3">
<p className="flex gap-2 items-center ">
<StarIcon />
<strong className="font-semibold">
{(Number(activeProperty?.average_space_rating) || 0).toFixed(1)}
<span className="font-normal">({activeProperty?.space_rating_count})</span>
</strong>
</p>
<button
className="text-sm underline whitespace-nowrap"
target="_blank"
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
setShowMap(true);
}}
>
(view on map)
</button>
</div>
<div className="mt-6 lg:mt-[50px] flex flex-wrap gap-[12px] whitespace-nowrap">
{formatAmenities(activeProperty?.amenities).slice(0, 3).map((am, idx) => (
<span
className="text-[14px] bg-[#F2F4F7] rounded-[3px] pt-[2px] px-[8px] pb-[3px] text-[#667085]"
key={idx}
>
{am}
</span>
))}
{formatAmenities(activeProperty?.amenities).length > 3 ? <span className="text-[14px] bg-[#F2F4F7] rounded-[3px] pt-[2px] px-[8px] pb-[3px] text-[#667085]">+{formatAmenities(activeProperty?.amenities).length - 3} more</span> : null}
</div>
</div>
</div>
<hr className="my-4" />
</div>
) : null}
{(activeRoom?.booking_id && activeRoom?.booking?.id) ? (
<>
<div
className="mb-[8px] rounded-lg bg-cover bg-center bg-no-repeat px-[8px] pb-[13px]"
style={{ backgroundImage: `url(${(activeRoom.booking?.image_url) ?? "/default-property.jpg"})`, height: 150 }}
>
<span className="px-2 py-1 mt-3 inline-flex text-white bg-black font-bold rounded-lg text-xs self-start">{activeRoom?.booking?.space_category ? activeRoom?.booking?.space_category : "N/A"}</span>
</div>
<div className="">
<div className="mb-6 flex justify-between">
<p>Date</p>
<p className="font-semibold">
{" "}
{monthsMapping[new Date(activeRoom?.booking?.booking_start_time).getMonth()] +
" " +
new Date(activeRoom?.booking?.booking_start_time).getDate() +
"/" +
new Date(activeRoom?.booking?.booking_start_time).getFullYear()}
</p>
</div>
<div className="mb-6 flex justify-between">
<p>Time</p>
<p className="font-semibold">
{formatAMPM(activeRoom.booking?.booking_start_time)} - {formatAMPM(activeRoom?.booking?.booking_end_time)}
</p>
</div>
<div className="flex justify-between">
<p>Duration</p>
<p className="font-semibold">{activeRoom?.booking?.duration / 3600} hours</p>
</div>
</div>
<hr className="my-4" />
<h4 className="mb-6 text-xl font-semibold">Add-ons:</h4>
<div className="">
{activeRoom?.booking?.add_ons?.map((addon, idx) => (
<div
className="mb-4 flex gap-[14px]"
key={idx}
>
<CircleCheckIcon />
<p>{addon.name}</p>
</div>
))}
</div>
<hr className="my-4" />
</>
) : null}
<div className="text-center">
{(activeRoom?.booking_id && activeRoom?.booking?.id) &&
<Link
to={"/account/my-bookings/" + activeRoom.booking?.id}
className="my-text-gradient text-xs font-semibold uppercase tracking-wider"
>
View booking
</Link>}
{activeRoom.id && !activeRoom?.booking?.id &&
<Link
to={"/property/" + activeProperty?.id}
className="my-text-gradient text-xs font-semibold uppercase tracking-wider"
>
View property
</Link>
}
</div>
</div>
</div>
</div>
</div>
{showEmoji && (
<div className="absolute w-full h-full left-0 right-0">
<div style={{"left":"50px !important"}} className="emoji-picker flex w-full h-full items-center justify-center">
<EmojiPicker
className="absolute -top-10 md:top-10 z-[1000] bottom-0 left-0 right-0"
onEmojiClick={(em) => {
setMessage((prev) => prev + em.emoji);
setShowEmoji(false);
}}
searchDisabled
/>
</div>
</div>
)}
{showImagePreviewModal &&
<ImagePreviewModal activeRoom={activeRoom} getRooms={()=>getRooms()} state={state} setMessages={setMessages} setActiveRoom={setActiveRoom} setRooms={setRooms} spaceId={spaceId} activeBooking={activeBooking} setShowImagePreviewModal={setShowImagePreviewModal}/>
}
{activeRoom?.booking_id === null &&
<PropertySpaceMapImage
modalImage={`https://maps.googleapis.com/maps/api/staticmap?center=${activeProperty.address_line_1 || ""}, ${activeProperty.address_line_2 || ""}, ${activeProperty.city || ""}, ${activeProperty.country || ""
}&zoom=15&size=600x400&maptype=roadmap&markers=color:red|${activeProperty.address_line_1 || ""}, ${activeProperty.address_line_2 || ""}
&key=${import.meta.env.VITE_GOOGLE_API_KEY}`}
modalOpen={showMap}
closeModal={() => setShowMap(false)}
/>
}
</div>
</>
);
};
export default MessagesPage;
+405
View File
@@ -0,0 +1,405 @@
[
"2g1c",
"2 girls 1 cup",
"acrotomophilia",
"alabama hot pocket",
"alaskan pipeline",
"anal",
"anilingus",
"anus",
"apeshit",
"arsehole",
"ass",
"asshole",
"assmunch",
"auto erotic",
"autoerotic",
"babeland",
"baby batter",
"baby juice",
"ball gag",
"ball gravy",
"ball kicking",
"ball licking",
"ball sack",
"ball sucking",
"bangbros",
"bangbus",
"bareback",
"barely legal",
"barenaked",
"bastard",
"bastardo",
"bastinado",
"bbw",
"bdsm",
"beaner",
"beaners",
"beaver cleaver",
"beaver lips",
"beastiality",
"bestiality",
"big black",
"big breasts",
"big knockers",
"big tits",
"bimbos",
"birdlock",
"bitch",
"bitches",
"black cock",
"blonde action",
"blonde on blonde action",
"blowjob",
"blow job",
"blow your load",
"blue waffle",
"blumpkin",
"bollocks",
"bondage",
"boner",
"boob",
"boobs",
"booty call",
"brown showers",
"brunette action",
"bukkake",
"bulldyke",
"bullet vibe",
"bullshit",
"bung hole",
"bunghole",
"busty",
"butt",
"buttcheeks",
"butthole",
"camel toe",
"camgirl",
"camslut",
"camwhore",
"carpet muncher",
"carpetmuncher",
"chocolate rosebuds",
"cialis",
"circlejerk",
"cleveland steamer",
"clit",
"clitoris",
"clover clamps",
"clusterfuck",
"cock",
"cocks",
"coprolagnia",
"coprophilia",
"cornhole",
"coon",
"coons",
"creampie",
"cum",
"cumming",
"cumshot",
"cumshots",
"cunnilingus",
"cunt",
"darkie",
"date rape",
"daterape",
"deep throat",
"deepthroat",
"dendrophilia",
"dick",
"dildo",
"dingleberry",
"dingleberries",
"dirty pillows",
"dirty sanchez",
"doggie style",
"doggiestyle",
"doggy style",
"doggystyle",
"dog style",
"dolcett",
"domination",
"dominatrix",
"dommes",
"donkey punch",
"double dong",
"double penetration",
"dp action",
"dry hump",
"dvda",
"eat my ass",
"ecchi",
"ejaculation",
"erotic",
"erotism",
"escort",
"eunuch",
"fag",
"faggot",
"fecal",
"felch",
"fellatio",
"feltch",
"female squirting",
"femdom",
"figging",
"fingerbang",
"fingering",
"fisting",
"foot fetish",
"footjob",
"frotting",
"fuck",
"fuck buttons",
"fuckin",
"fucking",
"fucktards",
"fudge packer",
"fudgepacker",
"futanari",
"gangbang",
"gang bang",
"gay sex",
"genitals",
"giant cock",
"girl on",
"girl on top",
"girls gone wild",
"goatcx",
"goatse",
"god damn",
"gokkun",
"golden shower",
"goodpoop",
"goo girl",
"goregasm",
"grope",
"group sex",
"g-spot",
"guro",
"hand job",
"handjob",
"hard core",
"hardcore",
"hentai",
"homoerotic",
"honkey",
"hooker",
"horny",
"hot carl",
"hot chick",
"how to kill",
"how to murder",
"huge fat",
"humping",
"incest",
"intercourse",
"jack off",
"jail bait",
"jailbait",
"jelly donut",
"jerk off",
"jigaboo",
"jiggaboo",
"jiggerboo",
"jizz",
"juggs",
"kike",
"kinbaku",
"kinkster",
"kinky",
"knobbing",
"leather restraint",
"leather straight jacket",
"lemon party",
"livesex",
"lolita",
"lovemaking",
"make me come",
"male squirting",
"masturbate",
"masturbating",
"masturbation",
"menage a trois",
"milf",
"missionary position",
"mong",
"motherfucker",
"mound of venus",
"mr hands",
"muff diver",
"muffdiving",
"nambla",
"nawashi",
"negro",
"neonazi",
"nigga",
"nigger",
"nig nog",
"nimphomania",
"nipple",
"nipples",
"nsfw",
"nsfw images",
"nude",
"nudity",
"nutten",
"nympho",
"nymphomania",
"octopussy",
"omorashi",
"one cup two girls",
"one guy one jar",
"orgasm",
"orgy",
"paedophile",
"paki",
"panties",
"panty",
"pedobear",
"pedophile",
"pegging",
"penis",
"phone sex",
"piece of shit",
"pikey",
"pissing",
"piss pig",
"pisspig",
"playboy",
"pleasure chest",
"pole smoker",
"ponyplay",
"poof",
"poon",
"poontang",
"punany",
"poop chute",
"poopchute",
"porn",
"porno",
"pornography",
"prince albert piercing",
"pthc",
"pubes",
"pussy",
"queaf",
"queef",
"quim",
"raghead",
"raging boner",
"rape",
"raping",
"rapist",
"rectum",
"reverse cowgirl",
"rimjob",
"rimming",
"rosy palm",
"rosy palm and her 5 sisters",
"rusty trombone",
"sadism",
"santorum",
"scat",
"schlong",
"scissoring",
"semen",
"sex",
"sexcam",
"sexo",
"sexy",
"sexual",
"sexually",
"sexuality",
"shaved beaver",
"shaved pussy",
"shemale",
"shibari",
"shit",
"shitblimp",
"shitty",
"shota",
"shrimping",
"skeet",
"slanteye",
"slut",
"s&m",
"smut",
"snatch",
"snowballing",
"sodomize",
"sodomy",
"spastic",
"spic",
"splooge",
"splooge moose",
"spooge",
"spread legs",
"spunk",
"strap on",
"strapon",
"strappado",
"strip club",
"style doggy",
"suck",
"sucks",
"suicide girls",
"sultry women",
"swastika",
"swinger",
"tainted love",
"taste my",
"tea bagging",
"threesome",
"throating",
"thumbzilla",
"tied up",
"tight white",
"tit",
"tits",
"titties",
"titty",
"tongue in a",
"topless",
"tosser",
"towelhead",
"tranny",
"tribadism",
"tub girl",
"tubgirl",
"tushy",
"twat",
"twink",
"twinkie",
"two girls one cup",
"undressing",
"upskirt",
"urethra play",
"urophilia",
"vagina",
"venus mound",
"viagra",
"vibrator",
"violet wand",
"vorarephilia",
"voyeur",
"voyeurweb",
"voyuer",
"vulva",
"wank",
"wetback",
"wet dream",
"white power",
"whore",
"worldsex",
"wrapping men",
"wrinkled starfish",
"xx",
"xxx",
"yaoi",
"yellow showers",
"yiffy",
"zoophilia",
"🖕"
]