• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mozilla / fx-private-relay / a2bc0383-1205-4ebd-979f-e7ee6dba9a0d

18 Dec 2023 05:15PM UTC coverage: 73.514% (-0.7%) from 74.258%
a2bc0383-1205-4ebd-979f-e7ee6dba9a0d

push

circleci

jwhitlock
Add provider_id="" to SocialApp init

1962 of 2913 branches covered (0.0%)

Branch coverage included in aggregate %.

6273 of 8289 relevant lines covered (75.68%)

19.91 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

78.29
/frontend/src/components/dashboard/aliases/AliasList.tsx
1
import { useState, useEffect } from "react";
2✔
2
import styles from "./AliasList.module.scss";
2✔
3
import { AliasData, isRandomAlias } from "../../../hooks/api/aliases";
2✔
4
import { ProfileData } from "../../../hooks/api/profile";
5
import { Alias } from "./Alias";
2✔
6
import { filterAliases } from "../../../functions/filterAliases";
2✔
7
import { CategoryFilter, SelectedFilters } from "./CategoryFilter";
2✔
8
import { UserData } from "../../../hooks/api/user";
9
import { RuntimeData } from "../../../hooks/api/runtimeData";
10
import { useLocalLabels } from "../../../hooks/localLabels";
2✔
11
import { AliasGenerationButton } from "./AliasGenerationButton";
2✔
12
import { SearchIcon } from "../../Icons";
2✔
13
import { useFlaggedAnchorLinks } from "../../../hooks/flaggedAnchorLinks";
2✔
14
import { useL10n } from "../../../hooks/l10n";
2✔
15
import { Localized } from "../../Localized";
2✔
16
import { VisuallyHidden } from "../../VisuallyHidden";
2✔
17
import { MaskCard } from "./MaskCard";
2✔
18
import { isFlagActive } from "../../../functions/waffle";
2✔
19

20
export type Props = {
21
  aliases: AliasData[];
22
  profile: ProfileData;
23
  user: UserData;
24
  runtimeData?: RuntimeData;
25
  onCreate: (
26
    options:
27
      | { mask_type: "random" }
28
      | { mask_type: "custom"; address: string; blockPromotionals: boolean },
29
  ) => void;
30
  onUpdate: (alias: AliasData, updatedFields: Partial<AliasData>) => void;
31
  onDelete: (alias: AliasData) => void;
32
  onboarding?: boolean;
33
  children?: React.ReactNode;
34
};
35

36
/**
37
 * Display a list of <Alias> cards, with the ability to filter them or create a new alias.
38
 */
39
export const AliasList = (props: Props) => {
52✔
40
  const l10n = useL10n();
164✔
41
  const [stringFilterInput, setStringFilterInput] = useState("");
164✔
42
  const [stringFilterVisible, setStringFilterVisible] = useState(false);
163✔
43
  const [resetCheckBoxes, setCheckboxes] = useState(false);
163✔
44
  const [categoryFilters, setCategoryFilters] = useState<SelectedFilters>({});
163✔
45
  const [localLabels, storeLocalLabel] = useLocalLabels();
163✔
46
  const [generatedAlias, setGeneratedAlias] = useState<AliasData | undefined>(
163✔
47
    undefined,
48
  );
49
  const { onboarding = false } = props;
163✔
50
  // When <AliasList> gets added to the page, if there's an anchor link in the
51
  // URL pointing to a mask, scroll to that mask:
52
  useFlaggedAnchorLinks(
163✔
53
    [],
54
    props.aliases.map((alias) => encodeURIComponent(alias.full_address)),
206✔
55
  );
56
  const [openAlias, setOpenAlias] = useState<AliasData | undefined>(
163✔
57
    // If the mask was focused on by an anchor link, expand that one on page load:
58
    props.aliases.find(
59
      (alias) =>
60
        alias.full_address ===
206✔
61
        decodeURIComponent(document.location.hash.substring(1)),
62
    ),
63
  );
64
  const [existingAliases, setExistingAliases] = useState<AliasData[]>(
163✔
65
    props.aliases,
66
  );
67

68
  useEffect(() => {
163✔
69
    if (props.aliases.length === 0) {
52✔
70
      setOpenAlias(undefined);
1✔
71
    } else {
72
      const existingAliasIds = existingAliases.map((alias) => alias.id);
85✔
73
      const newAliases = props.aliases.filter(
51✔
74
        (alias) => existingAliasIds.indexOf(alias.id) === -1,
85✔
75
      );
76
      if (newAliases.length !== 0) {
51!
77
        setOpenAlias(newAliases[0]);
×
78
      }
79
    }
80
    setExistingAliases(props.aliases);
52✔
81
  }, [props.aliases, existingAliases]);
82

83
  if (props.aliases.length === 0) {
163✔
84
    return null;
1✔
85
  }
86

87
  const aliasesWithLocalLabels = props.aliases.map((alias) => {
162✔
88
    const aliasWithLocalLabel = { ...alias };
206✔
89
    if (
206✔
90
      alias.description.length === 0 &&
412✔
91
      props.profile.server_storage === false &&
92
      localLabels !== null
93
    ) {
94
      const localLabel = localLabels.find(
52✔
95
        (localLabel) =>
96
          localLabel.id === alias.id &&
51✔
97
          localLabel.mask_type === alias.mask_type,
98
      );
99
      if (localLabel !== undefined) {
52✔
100
        aliasWithLocalLabel.description = localLabel.description;
51✔
101
      }
102
    }
103
    return aliasWithLocalLabel;
206✔
104
  });
105

106
  const aliases = sortAliases(
162✔
107
    filterAliases(aliasesWithLocalLabels, {
108
      ...categoryFilters,
109
      string: stringFilterInput,
110
    }),
111
  );
112

113
  const findAliasDataFromPrefix = (
162✔
114
    aliasPrefix: string,
115
  ): AliasData | undefined => {
116
    return aliases.find((alias) => aliasPrefix === alias.address);
×
117
  };
118

119
  const aliasCards = aliases.map((alias) => {
162✔
120
    const onUpdate = (updatedFields: Partial<AliasData>) => {
148✔
121
      if (
4✔
122
        localLabels !== null &&
8✔
123
        typeof updatedFields.description === "string" &&
124
        props.profile.server_storage === false
125
      ) {
126
        storeLocalLabel(alias, updatedFields.description);
1✔
127
        delete updatedFields.description;
1✔
128
      }
129
      return props.onUpdate(alias, updatedFields);
4✔
130
    };
131

132
    const onChangeOpen = (isOpen: boolean) => {
148✔
133
      if (isOpen === true) {
×
134
        setOpenAlias(alias);
×
135
      } else if (openAlias !== undefined && openAlias.id === alias.id) {
×
136
        setOpenAlias(undefined);
×
137
      }
138
    };
139

140
    return (
148✔
141
      <li
142
        className={styles["alias-card-wrapper"]}
143
        key={alias.address + isRandomAlias(alias)}
144
        id={encodeURIComponent(alias.full_address)}
145
      >
146
        {isFlagActive(props.runtimeData, "mask_redesign") ? (
147
          <MaskCard
2✔
148
            mask={alias}
149
            user={props.user}
150
            profile={props.profile}
151
            onUpdate={onUpdate}
152
            onDelete={() => props.onDelete(alias)}
×
153
            isOpen={
154
              openAlias !== undefined &&
2!
155
              openAlias.id === alias.id &&
156
              openAlias.mask_type === alias.mask_type
157
            }
158
            onChangeOpen={onChangeOpen}
159
            showLabelEditor={
160
              props.profile.server_storage || localLabels !== null
2!
161
            }
162
            runtimeData={props.runtimeData}
163
            isOnboarding={onboarding}
164
            copyAfterMaskGeneration={generatedAlias?.id === alias.id}
165
          >
166
            {props.children}
167
          </MaskCard>
168
        ) : (
169
          <Alias
170
            alias={alias}
171
            user={props.user}
172
            profile={props.profile}
173
            onUpdate={onUpdate}
174
            onDelete={() => props.onDelete(alias)}
1✔
175
            isOpen={
176
              openAlias !== undefined &&
146!
177
              openAlias.id === alias.id &&
178
              openAlias.mask_type === alias.mask_type
179
            }
180
            onChangeOpen={onChangeOpen}
181
            showLabelEditor={
182
              props.profile.server_storage || localLabels !== null
176✔
183
            }
184
            runtimeData={props.runtimeData}
185
          />
186
        )}
187
      </li>
188
    );
189
  });
190

191
  // With at most five aliases, filters aren't really useful
192
  // for non-Premium users.
193
  const categoryFilter = props.profile.has_premium ? (
162✔
194
    <div className={styles["category-filter"]}>
195
      <CategoryFilter
196
        onChange={setCategoryFilters}
197
        selectedFilters={categoryFilters}
198
        resetChecks={resetCheckBoxes}
199
        setCheckboxes={setCheckboxes}
200
      />
201
    </div>
202
  ) : null;
203

204
  const emptyStateMessage =
205
    props.aliases.length > 0 && aliases.length === 0 ? (
162✔
206
      <Localized
207
        id="profile-filter-no-results"
208
        elems={{
209
          "clear-button": (
210
            <button
211
              onClick={() => {
212
                setCategoryFilters({});
×
213
                setCheckboxes(true);
×
214
                setStringFilterInput("");
×
215
              }}
216
              className={styles["clear-filters-button"]}
217
            />
218
          ),
219
        }}
220
      >
221
        <p className={styles["empty-state-message"]} />
222
      </Localized>
223
    ) : null;
224

225
  const handleOnChange = (value: string) => {
162✔
226
    // removes Unicode characters found within the range of 0080 to FFFF
227
    setStringFilterInput(value.replace(/[\u{0080}-\u{FFFF}]/gu, ""));
104✔
228
  };
229

230
  return (
231
    <section className={styles["alias-list-container"]}>
232
      {isFlagActive(props.runtimeData, "free_user_onboarding") &&
162!
233
      onboarding ? null : (
×
234
        <div className={styles.controls}>
235
          <div
236
            className={`${styles["string-filter"]} ${
237
              stringFilterVisible ? styles["is-visible"] : ""
162!
238
            }`}
239
          >
240
            <VisuallyHidden>
241
              <label htmlFor="stringFilter">
242
                {l10n.getString("profile-filter-search-placeholder-2")}
243
              </label>
244
            </VisuallyHidden>
245
            <input
246
              value={stringFilterInput}
247
              onChange={(e) => handleOnChange(e.target.value)}
104✔
248
              type="search"
249
              name="stringFilter"
250
              id="stringFilter"
251
              placeholder={l10n.getString(
252
                "profile-filter-search-placeholder-2",
253
              )}
254
            />
255
            <span className={styles["match-count"]}>
256
              {aliases.length}/{props.aliases.length}
257
            </span>
258
          </div>
259
          <button
260
            onClick={() => setStringFilterVisible(!stringFilterVisible)}
×
261
            title={l10n.getString("profile-filter-search-placeholder-2")}
262
            className={`${styles["string-filter-toggle"]} ${
263
              stringFilterVisible ? styles["active"] : ""
162!
264
            }`}
265
          >
266
            <SearchIcon
267
              alt={l10n.getString("profile-filter-search-placeholder-2")}
268
              width={20}
269
              height={20}
270
            />
271
          </button>
272
          {categoryFilter}
273
          <div className={styles["new-alias-button"]}>
274
            <AliasGenerationButton
275
              aliases={props.aliases}
276
              profile={props.profile}
277
              runtimeData={props.runtimeData}
278
              onCreate={props.onCreate}
279
              onUpdate={props.onUpdate}
280
              findAliasDataFromPrefix={findAliasDataFromPrefix}
281
              setGeneratedAlias={setGeneratedAlias}
282
            />
283
          </div>
284
        </div>
285
      )}
286
      <ul>
287
        {isFlagActive(props.runtimeData, "free_user_onboarding") && onboarding
324!
288
          ? aliasCards[0]
289
          : aliasCards}
290
      </ul>
291
      {emptyStateMessage}
292
    </section>
293
  );
294
};
295

296
function sortAliases(aliases: AliasData[]): AliasData[] {
297
  const aliasDataCopy = aliases.slice();
162✔
298
  aliasDataCopy.sort((aliasA, aliasB) => {
162✔
299
    // `Date.parse` can be inconsistent,
300
    // but should be fairly reliable in parsing ISO 8601 strings
301
    // (though if Temporal ever gets accepted by TC39, we should switch to that):
302
    const aliasATimestamp = Date.parse(aliasA.created_at);
40✔
303
    const aliasBTimestamp = Date.parse(aliasB.created_at);
40✔
304
    return aliasBTimestamp - aliasATimestamp;
40✔
305
  });
306
  return aliasDataCopy;
162✔
307
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc