Skip to content

Commit

Permalink
Mail
Browse files Browse the repository at this point in the history
Close #1
  • Loading branch information
nonylene committed Feb 7, 2024
1 parent 080f8ad commit 6548c56
Show file tree
Hide file tree
Showing 14 changed files with 624 additions and 22 deletions.
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
HEINEKEN_ELASTIC_SEARCH_URL="http://host.docker.internal:9200/"
HEINEKEN_ELASTIC_SEARCH_PUKIWIKI_INDEX="pukiwiki"
HEINEKEN_ELASTIC_SEARCH_MAIL_INDEX="mail"

HEINEKEN_PUKIWIKI_BASE_URL="https://inside.kmc.gr.jp/wiki/"

HEINEKEN_MAIL_DEFAULT_CATEGORIES="info,"
HEINEKEN_MAIL_BASE_URL="https://inside.kmc.gr.jp/m2w/"
19 changes: 9 additions & 10 deletions app/components/sort-button/index.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
import { useEffect, useState } from "react";
import Select from "react-select";

const sortOrderOptions = [
{ value: "s", label: "Score" },
{ value: "m", label: "Modified" },
{ value: "ta", label: "Title asc" },
{ value: "td", label: "Title desc" },
];
interface SortOrderOption {
value: string;
label: string;
}

const getOrderOption = (val: string) => {
const option = sortOrderOptions.find(({ value }) => value === val);
const getOrderOption = (options: SortOrderOption[], val: string) => {
const option = options.find(({ value }) => value === val);
if (option === undefined) {
throw Error(`value ${val} is not found on sortOrderOptions`);
}
return option;
};

interface SortButtonProps {
options: SortOrderOption[];
defaultOrder: string;
onNewOrder: (order: string) => void;
}
Expand All @@ -33,8 +32,8 @@ export default function SortButton(props: SortButtonProps) {
<>
<div className="col-auto ms-auto text-end ps-0">
<Select
options={sortOrderOptions}
value={getOrderOption(order)}
options={props.options}
value={getOrderOption(props.options, order)}
onChange={(e) => {
setOrder(order);
props.onNewOrder(e!.value);
Expand Down
154 changes: 154 additions & 0 deletions app/routes/search.mail/els-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { Message, MessageResult } from "./models";
import { toQueryString } from "~/utils";

export const SEARCH_SIZE = 35;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function createMessageFromResponse(json: any): Message {
return {
id: json._id,
category: json._source.category,
index: json._source.index,
from: json._source.from,
to: json._source.to,
subject: json._source.subject,
date: json._source.date,
body: json.highlight.body[0],
};
}

// From elasticsearch
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function createMessageResultFromResponse(json: any): MessageResult {
return {
messages: json.hits.hits.map((item: unknown) =>
createMessageFromResponse(item),
),
totalCount: json.hits.total.value,
};
}

export function buildMailSearch(
order: string,
page: number,
categories: string[],
useRawQuery: boolean,
query?: string,
) {
let queryString: string;
if (query == null || query == "") {
queryString = "*";
} else {
queryString = useRawQuery ? query : toQueryString(query);
}
return {
query: queryString,
size: SEARCH_SIZE,
from: (page - 1) * SEARCH_SIZE,
order: order,
categories: categories,
};
}

interface MailSearch {
query: string;
size: number;
from: number;
order: string;
categories: string[];
}

export async function requestSearch({
query,
size,
from,
order,
categories,
}: MailSearch) {
// boot by some fields
const fields = ["subject^3", "from^4", "to^4", "body"];

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const queryJson: any = {
query: {
bool: {
must: {
function_score: {
query: {
query_string: {
fields: fields,
query: query,
default_operator: "AND",
},
},
},
},

filter: {
terms: {
category: categories,
},
},
},
},
_source: ["subject", "from", "to", "date", "category", "index"],
size: size,
from: from,
};

// eslint-disable-next-line default-case
switch (order) {
case "d":
queryJson["sort"] = { date: "desc" };
break;
case "s":
queryJson["sort"] = ["_score", { date: "desc" }];
queryJson["query"]["bool"]["must"]["function_score"]["functions"] = [
{
// boost by date
exp: {
date: {
// tekitou iikanji values~~
offset: "150d",
scale: "500d",
decay: 0.75,
},
},
},
];
break;
}

queryJson["highlight"] = {
// html escape
encoder: "html",
fields: {
body: {
pre_tags: ["<mark>"],
post_tags: ["</mark>"],
fragment_size: 220,
no_match_size: 220,
number_of_fragments: 1,
},
},
};

const url = new URL(
"_search",
process.env.HEINEKEN_ELASTIC_SEARCH_URL! +
process.env.HEINEKEN_ELASTIC_SEARCH_MAIL_INDEX! +
"/",
);
url.searchParams.append("source", JSON.stringify(queryJson));
url.searchParams.append("source_content_type", "application/json");

const response = await fetch(url);
if (!response.ok) {
throw new Response(
`Invalid response from the backend elasticsearch server: ${response.statusText}`,
{ status: 500 },
);
}
const json = await response.json();
return createMessageResultFromResponse(json);
}
14 changes: 14 additions & 0 deletions app/routes/search.mail/message-list.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.MessageListItem a {
display: inline-block;
text-decoration: none;
max-width: 100%;
}
.MessageListItem a h3:hover {
text-decoration: underline;
}

.MessageListItem p.small {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
87 changes: 87 additions & 0 deletions app/routes/search.mail/message-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import styles from "./message-list.css";
import { Message, MessageResult } from "./models";
import { createM2WUrl } from "./utils";
import { faCalendar, faEnvelope } from "@fortawesome/free-regular-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { LinksFunction } from "@remix-run/node";
import { truncate } from "~/utils";

export const links: LinksFunction = () => [{ rel: "stylesheet", href: styles }];

interface MessageListItemProps {
message: Message;
mailBaseURL: string;
}

function MessageListItem(props: MessageListItemProps) {
const dateStr = new Date(props.message.date).toLocaleString("ja-JP", {
timeZone: "Asia/Tokyo",
});
const message = props.message;
return (
<div className="MessageListItem mb-2">
<div className="row">
<div className="col-md-10 offset-md-1">
<a
href={createM2WUrl(
props.mailBaseURL,
props.message.category,
props.message.index,
)}
>
<h3>
{
// Some mails have very long subjects.
truncate(props.message.subject, 160)
}
</h3>
</a>
</div>
</div>

<div className="row">
<div className="col-md-10 offset-md-1">
<p className="small mb-1">
<FontAwesomeIcon icon={faEnvelope} />
&ensp;{message.category}: {message.from}&ensp;&#x279C;&ensp;
{message.to.join(", ")}
</p>
</div>
</div>

<div className="row">
<div className="col-md-10 offset-md-1">
<p className="small mb-2">
<FontAwesomeIcon icon={faCalendar} />
&ensp;{dateStr}
</p>
</div>
</div>

<div className="row">
<div className="col-md-10 offset-md-1">
<p dangerouslySetInnerHTML={{ __html: message.body }} />
</div>
</div>
</div>
);
}

interface MessageListProps {
messageResult: MessageResult;
mailBaseURL: string;
}

export function MessageList(props: MessageListProps) {
return (
<div className="MessageList">
{props.messageResult.messages.map((item) => (
<MessageListItem
key={item.id}
message={item}
mailBaseURL={props.mailBaseURL}
/>
))}
</div>
);
}
15 changes: 15 additions & 0 deletions app/routes/search.mail/models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export interface Message {
id: string;
category: string;
index: 0;
from: string;
to: string[];
subject: string;
body: string;
date: number;
}

export interface MessageResult {
messages: Message[];
totalCount: number;
}
Loading

0 comments on commit 6548c56

Please sign in to comment.