Tinder Deblur

Simple script using the official Tinder API to get clean photos of the users who liked you

// ==UserScript==
// @name        Tinder Deblur
// @namespace   Violentmonkey Scripts
// @match       https://tinder.com/*
// @grant       none
// @version     5.0
// @author      Tajnymag
// @description Simple script using the official Tinder API to get clean photos of the users who liked you
// ==/UserScript==

// enable type checking
// @ts-check
// @filename: types/tampermonkey.d.ts

class UserCacheItem {
	/**
	 * @param {string} userId
	 * @param {object} user
	 */
	constructor(userId, user) {
		this.userId = userId;
		this.user = user;
		this.hidden = !!localStorage.getItem('hiddenUsers')?.includes(userId);

		this.photoIndex = 0;
	}

	/**
	 * @returns {string | null}
	 */
	getPreviousPhoto() {
		if (!this.user) return null;

		this.photoIndex = this.photoIndex - 1;

		if (this.photoIndex < 0) this.photoIndex = this.user.photos.length - 1;

		return this.user.photos[this.photoIndex].url;
	}

	/**
	 * @returns {string | null}
	 */
	getNextPhoto() {
		if (!this.user) return null;

		this.photoIndex = (this.photoIndex + 1) % this.user.photos.length;

		return this.user.photos[this.photoIndex].url;
	}

	/**
	 * @returns {number}
	 */
	getAge() {
		if (!this.user || !this.user.birth_date) return 0;

		const currentDate = new Date();
		const birthDate = Date.parse(this.user.birth_date);

		const currentYear = currentDate.getFullYear();
		const currentMonth = currentDate.getMonth();
		const currentDay = currentDate.getDay();
		const birthYear = birthDate.getFullYear();
		const birthMonth = birthDate.getMonth();
		const birthDay = birthDate.getDay();

		let age = currentYear - birthYear;

		if (currentMonth < birthMonth) age--;
		else if (currentDay < birthDay) age--;

		return age;
	}

	/**
	 * @returns {boolean}
	 */
	isHidden() {
		return this.hidden;
	}

	/** @param {boolean} hidden */
	setHidden(hidden) {
		this.hidden = hidden;
	}
}

class UserCache {
	constructor() {
		/** @type {Map<string, UserCacheItem>} */
		this.cache = new Map();
	}

	/**
	 * @param {string} userId
	 * @param {object} user
	 * @returns {UserCacheItem}
	 */
	add(userId, user) {
		this.delete(userId);

		const newItem = new UserCacheItem(userId, user);
		this.cache.set(userId, newItem);

		return newItem;
	}

	/**
	 * @param {string} userId
	 */
	has(userId) {
		return this.cache.has(userId);
	}

	/**
	 * @param {string} userId
	 * @returns UserCacheItem | undefined
	 */
	get(userId) {
		return this.cache.get(userId);
	}

	/**
	 * @param {string} userId
	 */
	delete(userId) {
		const existingUser = this.cache.get(userId);

		if (!existingUser) return;

		this.cache.delete(userId);
	}

	clear() {
		for (const userItem of this.cache.values()) {
			this.cache.delete(userItem.userId);
		}
	}
}

/**
 * Holds a persistent cache of fetched users and intervals for updating their photos
 */
const cache = new UserCache();

/**
 * Original function of the script
 */
async function unblur() {
	/** @type {HTMLElement | null} */
	const likesGridContainerEl = document.querySelector('main div.Expand > div[role="grid"]');

	if (!likesGridContainerEl) return;

	if (!likesGridContainerEl.dataset.loadingTextAdded) {
		likesGridContainerEl.dataset.loadingTextAdded = 'true';

		likesGridContainerEl.style.position = 'relative';

		const loadingContainer = document.createElement('DIV');
		loadingContainer.classList.add('loading-container');
		loadingContainer.setAttribute(
			'style',
			'align-items: center; background-color: black; display: flex; height: 100%; justify-content: center; left: 0; position: absolute; text-align: center; top: 0; width: 100%; z-index: 50;'
		);
		likesGridContainerEl.insertBefore(loadingContainer, likesGridContainerEl.firstChild);

		const loadingText = document.createElement('H4');
		loadingText.setAttribute(
			'style',
			'color: #d2d2d3; font-size: 40px; letter-spacing: 2px; text-transform: uppercase;'
		);
		loadingText.innerText = 'Loading';
		loadingContainer.appendChild(loadingText);
	}

	const [failedToFetchTeasersError, teasers] = await safeAwait(fetchTeasers());

	if (failedToFetchTeasersError) {
		console.error(`Could not load teasers: ${failedToFetchTeasersError.name}`);
		return;
	}

	/** @type {NodeListOf<HTMLElement>} */
	const teaserEls = document.querySelectorAll('.Expand.enterAnimationContainer > div:nth-child(1)');

	for (let i = 0; i < teaserEls.length; ++i) {
		const teaserUser = teasers[i].user;
		const teaserEl = teaserEls[i];
		const teaserImage = teaserUser.photos[0].url;

		if (!teaserEl) continue;

		const likeEl = teaserEl.parentElement?.parentElement;

		if (!likeEl) continue;

		if (!likeEl.classList.contains('like')) likeEl.classList.add('like');

		const likeElContent = likeEl.querySelector('.enterAnimationContainer.Expand');

		if (!likeElContent) continue;

		if (teaserImage.includes('unknown') || !teaserImage.includes('images-ssl')) {
			if (likeEl.dataset.invalid) continue;

			likeEl.dataset.invalid = 'true';
			likeElContent.style.opacity = '0.5';
			likeEl.innerHTML += `
				<div class="invalid-text" style="align-items: center; display: flex; flex-direction: column; font-size: 14px; height: calc(100% - 25px * 2); gap: 15px; left: 25px; text-align: center; top: 25px; position: absolute; width: calc(100% - 25px * 2);">
					<span style="background-color: #0008; border-radius: 12.5px; color: #ac0c04; letter-spacing: 1px; padding: 5px 14px; text-transform: uppercase;">Unable to deblur</span>
					<span class="invalid-disclaimer" style="background-color: #0004; border-radius: 12.5px; padding: 15px 20px;"><b>This is not a bug!</b><br />Not all likes can be unblurred.</span>
				</div>
			`;
			continue;
		}

		if (likeEl.dataset.invalid) {
			delete likeEl.dataset.invalid;

			likeEl.querySelector('.invalid-text').remove();
			likeElContent.style.opacity = '1';
		}

		const userId = teaserImage.slice(32, 56);

		if (cache.has(userId)) continue;

		try {
			// only update teaser once
			if (likeEl.dataset.userId) continue;

			const infoContainerEl = teaserEl.parentElement?.lastElementChild;

			if (!infoContainerEl) {
				console.error(`Could not find info container for '${userId}'`);
				return;
			}

			infoContainerEl.outerHTML = `
				<div class='Pos(a) Start(0) End(0) TranslateZ(0) Pe(n) B(0)' style='background-image: linear-gradient(to top, #000F 0%, #0000 100%); height: 65%;'>
					<div style='opacity: 0; transition: opacity 0.5s ease-out;' class='like-user-info Pos(a) D(f) Jc(sb) C($c-ds-text-primary-overlay) Ta(start) W(100%) Ai(fe) B(0) P(8px)--xs P(16px) P(20px)--l Cur(p) focus-button-style' tabindex='0'>
						<div class='Tsh($tsh-s) D(f) Fx($flx1) Fxd(c) Miw(0)'></div>
					</div>
				</div>
			`;

			likeEl.classList.add('like-item');
			likeEl.dataset.userId = userId;

			teaserEl.id = 'teaser-' + userId;
			teaserEl.classList.add('teaser', 'like-action-button', 'like-action-next-photo');
			teaserEl.style.backgroundSize = 'cover';

			fetchUser(userId).then((user) => {
				// save user to cache
				const userItem = cache.add(userId, user ?? null);

				// hide the like if it was passed before
				if (userItem.isHidden()) {
					likeEl.remove();
					return;
				}

				if (!user) {
					teaserEl.style.backgroundImage = `url('https://preview.gotinder.com/${teaserUser._id}/original_${teaserUser.photos[0].id}.jpeg')`;
					infoContainerEl.remove();

					console.error(`Could not load user '${userId}'`);
					return;
				}

				// log user name + bio
				console.debug(`${user.name} (${user.bio})`);

				const likeUserInfo = infoContainerEl.querySelector('like-user-info')?.firstChild;

				if (!likeUserInfo) return;

				likeUserInfo.innerHTML = `
					<div class='Pos(a) Fz($l) B(0) Trsdu($fast) Maw(80%) D(f) Fxd(c) like-user-name'>
						<div class='D(f) Ai(c) Miw(0)'>
							<!-- Name -->
							<div class='Ov(h) Ws(nw) As(b) Ell'>
								<span class='Typs(display-2-strong)' itemprop='name'>${user.name}</span>
							</div>
							<span class='As(b) Pend(8px)'></span>
							<!-- Age -->
							<span class='As(b)' itemprop='age'>${userItem.getAge()}</span>
						</div>
					</div>
					<!-- Distance -->
					<div class="D(f) Row Typs(body-1-regular)" style="transform: translateY(-20px);">
						<div class="D(ib) Va(t)">
							<svg focusable="false" aria-hidden="true" role="presentation" viewBox="0 0 24 24" width="24px" height="24px" class="Va(m) Sq(16px)">
								<g fill="#fff" stroke="#fff" stroke-width=".5" fill-rule="evenodd">
									<path d="M11.436 21.17l-.185-.165a35.36 35.36 0 0 1-3.615-3.801C5.222 14.244 4 11.658 4 9.524 4 5.305 7.267 2 11.436 2c4.168 0 7.437 3.305 7.437 7.524 0 4.903-6.953 11.214-7.237 11.48l-.2.167zm0-18.683c-3.869 0-6.9 3.091-6.9 7.037 0 4.401 5.771 9.927 6.897 10.972 1.12-1.054 6.902-6.694 6.902-10.95.001-3.968-3.03-7.059-6.9-7.059h.001z" />
									<path d="M11.445 12.5a2.945 2.945 0 0 1-2.721-1.855 3.04 3.04 0 0 1 .641-3.269 2.905 2.905 0 0 1 3.213-.645 3.003 3.003 0 0 1 1.813 2.776c-.006 1.653-1.322 2.991-2.946 2.993zm0-5.544c-1.378 0-2.496 1.139-2.498 2.542 0 1.404 1.115 2.544 2.495 2.546a2.52 2.52 0 0 0 2.502-2.535 2.527 2.527 0 0 0-2.499-2.545v-.008z" />
								</g>
							</svg>
						</div>
						<div class="Us(t) Va(m) D(ib) NetWidth(100%,24px) C($c-ds-text-secondary-overlay) Ell">
							${user.distance_mi} miles away
						</div>
					</div>
					<!-- Bio -->
					<span class='like-user-bio' style='-webkit-box-orient: vertical; display: -webkit-box; -webkit-line-clamp: 3; max-height: 63px; overflow-y: hidden; text-overflow: ellipsis; transform: translateY(-20px);'>${
						user.bio
					}</span>
				`;

				teaserEl.style.backgroundImage = `url(${user.photos[0].url})`;
			});
		} catch (err) {
			if (err.name != 'SyntaxError') console.error(`Failed to load user '${userId}': ${err.name}`);
		}
	}
}

/**
 * Remove Tinder Gold ads
 */
function removeGoldAds() {
	// hide special offer advertisement
	const advertisementLogo = document.querySelector('div[aria-label="Tinder Gold"]');

	if (advertisementLogo) {
		const addContainer = advertisementLogo.parentElement?.parentElement;

		if (addContainer) addContainer.style.display = 'none';
	}

	// remove 'Tinder Gold' advertisement

	for (const advertisementEl of document.getElementsByTagName('div')) {
		if (advertisementEl.children.length > 0) continue;

		if (advertisementEl.innerText.toLowerCase().includes('gold')) advertisementEl.remove();
	}

	// remove gold button
	/** @type {HTMLButtonElement | null} */
	const goldButtonEl = document.querySelector('div.CenterAlign button[type="button"]');

	if (goldButtonEl != null) goldButtonEl.remove();
}

function updateUserInfos() {
	/** @type {HTMLElement | null} */
	const likesGridContainerEl = document.querySelector('main div.Expand > div[role="grid"]');

	if (!likesGridContainerEl) return;

	// fix scrolling
	if (likesGridContainerEl.parentElement) likesGridContainerEl.parentElement.style.overflow = 'hidden';

	if (!likesGridContainerEl.dataset.eventsInterrupted) {
		likesGridContainerEl.dataset.eventsInterrupted = 'true';
		likesGridContainerEl.addEventListener('scroll', (event) => event.stopImmediatePropagation(), true);
		likesGridContainerEl.style.justifyContent = 'flex-start';
	}

	// update the likes grid
	const likesGridEl = likesGridContainerEl.lastElementChild;

	if (!likesGridEl.dataset.stylesUpdated) {
		likesGridEl.dataset.stylesUpdated = 'true';
		likesGridEl.classList.add('D(f)');
		likesGridEl.style.removeProperty('height');
		likesGridEl.style.flexWrap = 'wrap';
		likesGridEl.style.flex = 'unset';
		likesGridEl.style.gap = '10px';
		likesGridEl.style.justifyContent = 'flex-start';
	}

	// update the like elements
	for (const likeEl of likesGridEl.children) {
		// don't update the element if it is invisible
		if (likeEl.style.display === 'none') continue;

		likeEl.classList.remove('Cur(p)');
		likeEl.style.removeProperty('transform');
		likeEl.style.position = 'relative';
		likeEl.style.backgroundColor = 'black';
		likeEl.style.borderRadius = '8px';
		likeEl.style.marginTop = '0';
		likeEl.style.marginBottom = '0';

		const userId = likeEl.dataset.userId;

		// only update if user was loaded
		if (!userId) continue;

		const userItem = cache.get(userId);

		if (!userItem) continue;

		// only update the container once
		if (likeEl.dataset.infoSet) continue;

		likeEl.dataset.infoSet = 'true';

		/** @type {HTMLElement | null} */
		const infoContainerEl = likeEl.querySelector('.like-user-info');

		if (!infoContainerEl) continue;

		// add action buttons
		likeEl.innerHTML += `
			<div class='like-actions' style='align-items: center; background-image: linear-gradient(to top, #0004, #0001); border-radius: 8px; display: flex; height: 30px; justify-content: space-around; left: 5px; padding: 2px; position: absolute; bottom: 5px; width: calc(100% - 5px * 2);'>
				<!-- Hide -->
				<button class='like-action-button like-action-pass button Lts($ls-s) Cur(p) Tt(u) Bdrs(50%) P(0) Fw($semibold) focus-button-style Bxsh($bxsh-btn) Wc($transform) Pe(a) Scale(1.1):h Scale(.9):a' type='button' style='cursor: pointer; height: 24px; width: 24px;' draggable='false'>
					<span class='Pos(r) Z(1) Expand'>
						<span class='D(b) Expand' style='transform: scale(1); filter: none;'>
							<svg focusable='false' aria-hidden='true' role='presentation' viewBox='0 0 24 24' width='24px' height='24px' class='Scale(.75) Expand'>
								<path d='m15.44 12 4.768 4.708c1.056.977 1.056 2.441 0 3.499-.813 1.057-2.438 1.057-3.413 0L12 15.52l-4.713 4.605c-.975 1.058-2.438 1.058-3.495 0-1.056-.813-1.056-2.44 0-3.417L8.47 12 3.874 7.271c-1.138-.976-1.138-2.44 0-3.417a1.973 1.973 0 0 1 3.25 0L12 8.421l4.713-4.567c.975-1.139 2.438-1.139 3.413 0 1.057.814 1.057 2.44 0 3.417L15.44 12Z' fill='var(--fill--background-nope, none)' />
							</svg>
						</span>
					</span>
				</button>
				<!-- Like -->
				<button class='like-action-button like-action-like button Lts($ls-s) Cur(p) Tt(u) Bdrs(50%) P(0) Fw($semibold) focus-button-style Bxsh($bxsh-btn) Wc($transform) Pe(a) Scale(1.1):h Scale(.9):a' type='button' style='cursor: pointer; height: 24px; width: 24px;' draggable='false'>
					<span class='Pos(r) Z(1) Expand'>
						<span class='D(b) Expand' style='transform: scale(1); filter: none;'>
							<svg focusable='false' aria-hidden='true' role='presentation' viewBox='0 0 24 24' width='24px' height='24px' class='Scale(.75) Expand'>
								<path d='M21.994 10.225c0-3.598-2.395-6.212-5.72-6.212-1.78 0-2.737.647-4.27 2.135C10.463 4.66 9.505 4 7.732 4 4.407 4 2 6.62 2 10.231c0 1.52.537 2.95 1.533 4.076l8.024 7.357c.246.22.647.22.886 0l7.247-6.58.44-.401.162-.182.168-.174a6.152 6.152 0 0 0 1.54-4.09' fill='var(--fill--background-like, none)' />
							</svg>
						</span>
					</span>
				</button>
			</div>
		`;

		// handle like element click
		likeEl.addEventListener(
			'click',
			(event) => {
				/** @type {HTMLElement | null} */
				let currentParent = event.target;

				if (!currentParent) return;

				while (!currentParent?.classList.contains('like-action-button')) {
					if (!currentParent?.parentElement) break;
					currentParent = currentParent.parentElement;
				}

				event.stopImmediatePropagation();

				if (!currentParent) return;

				if (currentParent.classList.contains('like-action-pass')) {
					pass(userItem);
				} else if (currentParent.classList.contains('like-action-like')) {
					like(userItem);
				} else {
					if (!userItem.user) return;

					if (currentParent.classList.contains('like-action-photo')) {
						const index = parseInt(currentParent.dataset.photoIndex ?? '0');
						showPhoto(likeEl, userItem.photoIndex, index, userItem.user.photos[index].url);
						userItem.photoIndex = index;
					} else if (currentParent.classList.contains('like-action-next-photo')) {
						const oldIndex = userItem.photoIndex;
						const photoUrl =
							event.offsetX < currentParent.clientWidth / 2
								? userItem.getPreviousPhoto()
								: userItem.getNextPhoto();
						showPhoto(likeEl, oldIndex, userItem.photoIndex, photoUrl);
					}

					return;
				}

				likeEl.remove();
			},
			true
		);

		/** @type {HTMLElement | null} */
		const userNameEl = infoContainerEl.querySelector('.like-user-name');
		/** @type {HTMLElement | null} */
		const userBioEl = infoContainerEl.querySelector('.like-user-bio');

		if (!userNameEl || !userBioEl) continue;

		const user = userItem.user;

		// update info container

		const userBioElHeight = userBioEl.getBoundingClientRect().height;

		userNameEl.style.transform = `translateY(-${
			userBioElHeight + 20 /* distance height */ + 20 /* name height */ + 20 /* action buttons */
		}px)`;
		infoContainerEl.style.opacity = `1`;

		// add photo selector
		const photoSelectorContainer = document.createElement('div');
		photoSelectorContainer.setAttribute(
			'class',
			'photo-selectors CenterAlign D(f) Fxd(r) W(100%) Px(8px) Pos(a) Iso(i)'
		);
		photoSelectorContainer.style.top = '5px';
		likeEl.appendChild(photoSelectorContainer);

		for (let i = 0; i < user.photos.length; i++) {
			const photoButton = document.createElement('button');
			photoButton.setAttribute(
				'class',
				'like-action-button like-action-photo bullet D(ib) Va(m) Cnt($blank)::a D(b)::a Cur(p) H(4px)::a W(100%)::a Py(4px) Px(2px) W(100%) Bdrs(100px)::a focus-background-style ' +
					(i == 0
						? 'Bgc($c-ds-background-tappy-indicator-active)::a bullet--active'
						: 'Bgc($c-ds-background-tappy-indicator-inactive)::a')
			);
			photoButton.dataset.photoIndex = i.toString();
			photoSelectorContainer.appendChild(photoButton);
		}
	}

	const totalLikesCount = likesGridEl?.childElementCount ?? 0;

	if (totalLikesCount == 0) {
		if (!likesGridContainerEl.dataset.noLikes) {
			likesGridContainerEl.dataset.noLikes = 'true';

			if (likesGridContainerEl.dataset.loadingTextAdded)
				likesGridContainerEl.querySelector('.loading-container')?.remove();

			const noLikesContainer = document.createElement('DIV');
			noLikesContainer.classList.add('no-likes-container');
			noLikesContainer.setAttribute(
				'style',
				'align-items: center; background-color: black; display: flex; height: 100%; justify-content: center; left: 0; position: absolute; text-align: center; top: 0; width: 100%; z-index: 50;'
			);
			likesGridContainerEl.insertBefore(noLikesContainer, likesGridContainerEl.firstChild);

			const noLikesText = document.createElement('H4');
			noLikesText.setAttribute(
				'style',
				'color: #d2d2d3; font-size: 40px; letter-spacing: 2px; text-transform: uppercase;'
			);
			noLikesText.innerText = 'No likes available';
			noLikesContainer.appendChild(noLikesText);
		}
	} else if (
		document.querySelectorAll('div[data-info-set]').length > 0 ||
		document.querySelectorAll('div[data-invalid]').length == totalLikesCount
	) {
		if (!likesGridContainerEl.dataset.loadingComplete) {
			likesGridContainerEl.dataset.loadingComplete = 'true';

			if (likesGridContainerEl.dataset.noLikes) {
				delete likesGridContainerEl.dataset.noLikes;

				likesGridContainerEl.querySelector('.no-likes-container')?.remove();
			}

			const loadingContainer = likesGridContainerEl.querySelector('.loading-container');

			if (!loadingContainer) return;

			loadingContainer.style.transition = 'opacity 0.4s 0.2s ease-out';
			loadingContainer.style.opacity = '0';

			setTimeout(() => loadingContainer.remove(), 600);
		}
	}
}

/**
 * Updates the photo
 * @param {HTMLElement} likeEl
 * @param {number} oldIndex
 * @param {number} index
 * @param {string} photoUrl
 */
function showPhoto(likeEl, oldIndex, index, photoUrl) {
	/** @type {HTMLElement | null} */
	const teaserEl = likeEl.querySelector('.teaser');
	const photoSelectorContainer = likeEl.querySelector('.photo-selectors');

	if (!photoSelectorContainer) return;

	const oldPhotoButton = photoSelectorContainer.children[oldIndex];
	oldPhotoButton.classList.remove('Bgc($c-ds-background-tappy-indicator-active)::a');
	oldPhotoButton.classList.remove('bullet--active');
	oldPhotoButton.classList.add('Bgc($c-ds-background-tappy-indicator-inactive)::a');

	if (!teaserEl) return;

	teaserEl.style.backgroundImage = `url('${photoUrl}')`;

	const newPhotoButton = photoSelectorContainer.children[index];
	newPhotoButton.classList.remove('Bgc($c-ds-background-tappy-indicator-inactive)::a');
	newPhotoButton.classList.add('Bgc($c-ds-background-tappy-indicator-active)::a');
	newPhotoButton.classList.add('bullet--active');
}

/**
 * Hides a user from the likes section
 * @param {UserCacheItem} userItem
 */
function hide(userItem) {
	const hiddenUsers = localStorage.getItem('hiddenUsers')?.split(';') ?? [];

	if (!hiddenUsers.includes(userItem.userId)) hiddenUsers.push(userItem.userId);

	localStorage.setItem('hiddenUsers', hiddenUsers.join(';'));

	userItem.hidden = true;
}

/**
 * Adds user filtering
 */
function updateUserFiltering() {
	/** @type {HTMLDivElement | null} */
	const filterButtonEl = document.querySelector('div[role="grid"] div[role="option"]:nth-of-type(1)');

	if (filterButtonEl != null) {
		if (!filterButtonEl.dataset.eventsInterrupted) {
			filterButtonEl.dataset.eventsInterrupted = 'true';
			filterButtonEl.addEventListener('click', () => {
				setTimeout(() => {
					// remove "show all" button
					for (const element of document.querySelectorAll(
						'div[role="dialog"] .menuItem__contents > div > div[role="button"]'
					)) {
						element.remove();
					}

					const applyContainer = document.querySelector(
						'div[role="dialog"] > div:not(.menuItem):not(.CenterAlign)'
					);

					if (applyContainer != null) {
						applyContainer.innerHTML = '';
						applyContainer.className = '';
						applyContainer.setAttribute(
							'style',
							'align-items: center; display: flex; flex-shrink: 0; font-size: 20px; height: 50px; justify-content: center; width: 100%;'
						);

						const applyButtonEl = document.createElement('button');
						applyButtonEl.innerText = 'Apply';
						applyButtonEl.style.textTransform = 'uppercase';
						applyButtonEl.style.fontWeight = '600';
						applyButtonEl.style.color = 'var(--color--text-brand-normal)';
						applyContainer.appendChild(applyButtonEl);

						applyButtonEl.addEventListener(
							'click',
							(event) => {
								event.stopImmediatePropagation();

								const dialogMenuItemContents = document.querySelectorAll(
									'div[role="dialog"] > .menuItem > .menuItem__contents > div:nth-of-type(2)'
								);

								// max distance
								const maxDistanceElement = dialogMenuItemContents[0].querySelector('div[style]');

								if (!maxDistanceElement) return;

								let maxDistance = Math.floor(
									(maxDistanceElement.clientWidth /
										(maxDistanceElement.parentElement?.clientWidth ?? 1)) *
										(161 - 2) +
										2
								);

								if (maxDistance == 161) maxDistance = Number.MAX_SAFE_INTEGER;

								// age range
								const ageRangeElement = dialogMenuItemContents[1].querySelector('div[style]');

								if (!ageRangeElement) return;

								const ageRangeStart = Math.round(
									(parseFloat(getComputedStyle(ageRangeElement).left.replace('px', '')) /
										(ageRangeElement.parentElement?.clientWidth ?? 1)) *
										(100 - 18) +
										18
								);
								let ageRangeEnd =
									ageRangeStart +
									Math.round(
										(ageRangeElement.clientWidth /
											(ageRangeElement.parentElement?.clientWidth ?? 1)) *
											(100 - 18)
									);

								if (ageRangeEnd == 100) ageRangeEnd = Number.MAX_SAFE_INTEGER;

								// minimum photos amount
								let minimumPhotosAmount = 0;

								/** @type {NodeListOf<HTMLDivElement>} */
								const photosOptions = dialogMenuItemContents[2].querySelectorAll('div[role="option"]');

								for (const minimumPhotosOption of photosOptions) {
									if (
										minimumPhotosOption
											.getAttribute('class')
											?.includes('c-ds-border-passions-shared')
									) {
										minimumPhotosAmount = parseInt(minimumPhotosOption.innerText);
										break;
									}
								}

								// interests
								const interests = [];

								/** @type {NodeListOf<HTMLDivElement>} */
								const interestOptions =
									dialogMenuItemContents[3].querySelectorAll('div[role="option"]');

								for (const interestOption of interestOptions) {
									if (interestOption.getAttribute('class')?.includes('c-ds-border-passions-shared'))
										interests.push(interestOption.innerText);
								}

								/** @type {NodeListOf<HTMLInputElement>} */
								const dialogMenuSelects = document.querySelectorAll(
									'div[role="dialog"] > .menuItem > .menuItem__contents .menuItem__select input'
								);

								// verified
								const verifiedRequired = dialogMenuSelects[0].checked;

								// has bio
								const bioRequired = dialogMenuSelects[1].checked;

								// apply filter
								/** @type {NodeListOf<HTMLDivElement>} */
								const likeItems = document.querySelectorAll('.like-item');

								for (const likeElement of likeItems) {
									if (!likeElement.dataset.userId) continue;

									const userItem = cache.get(likeElement.dataset.userId);

									if (!userItem) continue;

									const user = userItem.user;

									if (!user) continue;

									const userInterests = Array.from(user.user_interests ?? []).map(
										(interest) => interest.name
									);

									let matches = true;

									// check radius
									if (!user.hide_distance && user.distance_mi > maxDistance) matches = false;
									// check age range
									else if (
										!user.hide_age &&
										(userItem.getAge() < ageRangeStart || userItem.getAge() > ageRangeEnd)
									)
										matches = false;
									// check photos amount
									else if (user.photos.length < minimumPhotosAmount) matches = false;
									// check verified
									else if (!user.is_tinder_u && verifiedRequired) matches = false;
									// check bio
									else if (user.bio.length == 0 && bioRequired) matches = false;
									// check interests
									else {
										for (const interest of interests) {
											if (!userInterests.includes(interest)) matches = false;
										}
									}

									likeElement.style.display = matches ? 'flex' : 'none';
								}

								// close dialog
								/** @type {Element | null | undefined} */
								const applyButton =
									document.querySelector('div[role="dialog"]')?.parentElement?.firstElementChild;

								applyButton?.click();

								setTimeout(removeGoldAds, 250);
							},
							true
						);
					}
				}, 200);
			});
		}

		if (!filterButtonEl.parentElement) return;

		/** @type {NodeListOf<HTMLDivElement>} */
		const optionEls = filterButtonEl.parentElement.querySelectorAll('div[role="option"]');

		for (const optionEl of optionEls) {
			if (!optionEl.dataset.eventsInterrupted) optionEl.remove();
		}

		if (!filterButtonEl.parentElement.parentElement) return;

		/** @type {HTMLElement} */
		const filterButtonContainer = filterButtonEl.parentElement.parentElement;
		filterButtonContainer.style.maxWidth = 'calc(100% - 36.5px * 2 + 12px * 2)';
	}
}

/**
 * Creates a message status icon + text
 */
function createMessageStatusElement(parentNode, read) {
	if (parentNode == null) return;

	const status = document.createElement('div');
	status.setAttribute(
		'class',
		'Pos(r) Fz($2xs) My(8px) Mx(4px) Mih(16px) C($c-ds-text-secondary) D(f) Ai(c) Jc(fe) Fxd(r)'
	);
	status.innerHTML = `
		<div class="D(f) Jc(c) Fxd(r) Mend(8px) Ai(fs)">
			<svg focusable="false" aria-hidden="false" role="img" viewBox="0 0 24 24" width="24px" height="24px" class="Sq(12px)">
				<path d="M7.48 14.413l5.74-8.316a.63.63 0 01.9-.142l.917.697c.275.21.33.6.125.876L8.02 17.153a.63.63 0 01-.938.084l-.047-.044a.85.85 0 01-.145-.105l-4.072-3.653a.84.84 0 01-.075-1.173l.524-.612a.84.84 0 011.215-.063l2.996 2.826h.002zm6.353.627l5.747-8.327a.63.63 0 01.9-.143l.917.698c.275.209.33.6.125.877l-7.144 9.622a.63.63 0 01-.938.083l-.023-.023a.842.842 0 01-.217-.137l-2-1.738a.84.84 0 01-.087-1.182l.517-.6a.84.84 0 011.213-.065l.989.933.001.002z" fill="${
					read ? '#106bd5' : '#fff'
				}" fill-rule="evenodd" />
			</svg>
		</div>
		<span>${read ? 'Read' : 'Sent'}</span>
	`;

	parentNode.appendChild(status);
}

/**
 * Displays read status below sent messages
 */
async function updateMessageInfos(matchId) {
	/** @type {HTMLDivElement | null} */
	const lastMessageStatus = document.querySelector('.msg__status');

	if (!lastMessageStatus) return;

	lastMessageStatus.remove();

	fetchMatches().then((matches) => {
		if (matches == null) return;

		const filteredMatches = matches.filter((match) => match.id === matchId);

		if (filteredMatches.length == 0) return;

		const match = filteredMatches[0];
		const lastReadMesssageId = match.seen.last_seen_msg_id;

		if (!lastReadMesssageId) return;

		// get message content from last read message
		fetchMessages(matchId).then((messages) => {
			if (messages == null) return;

			const filteredMessages = messages.filter((message) => message._id === lastReadMesssageId);

			if (filteredMessages.length == 0) return;

			let lastReadMessage = filteredMessages[0];
			let currentMessageIndex = messages.indexOf(lastReadMessage);

			while (lastReadMessage.from === match.person._id && currentMessageIndex < messages.length - 1) {
				lastReadMessage = messages[currentMessageIndex++];
			}

			// only the matched person sent a message
			if (!lastReadMessage) return;

			const messageContent = lastReadMessage.message;

			/** @type {NodeListOf<HTMLElement>} */
			const messageElements = document.querySelectorAll('.msg');

			if (messageElements.length == 0) return;

			for (let i = messageElements.length - 1; i >= 0; i--) {
				const messageElement = messageElements[i];
				const messageContainer = messageElement.parentElement?.parentElement;

				if (!messageContainer) continue;

				const isRead = messageElement.innerText === messageContent;

				// only add info to messages sent by the user of this script
				if (messageContainer.classList.contains('Ta(e)')) createMessageStatusElement(messageContainer, isRead);

				if (isRead) break;
			}
		});
	});
}

/**
 * Passes a user and hides it from the likes section afterwards
 * @param {UserCacheItem} userItem
 */
async function pass(userItem) {
	const response = await fetch(
		`https://api.gotinder.com/pass/${userItem.userId}?s_number=${userItem.user?.s_number ?? 0}`,
		{
			headers: {
				'X-Auth-Token': localStorage.getItem('TinderWeb/APIToken') ?? '',
				platform: 'android',
			},
			method: 'GET',
		}
	);

	hide(userItem);
}

/**
 * Likes a user and hides it from the likes section afterwards
 * @param {UserCacheItem} userItem
 */
async function like(userItem) {
	const response = await fetch(`https://api.gotinder.com/like/${userItem.userId}`, {
		headers: {
			'X-Auth-Token': localStorage.getItem('TinderWeb/APIToken') ?? '',
			platform: 'android',
			'Content-Type': 'application/json',
		},
		method: 'POST',
		body: JSON.stringify(
			userItem.user
				? {
						liked_content_id: userItem.user.photos[0].id,
						liked_content_type: 'photo',
						s_number: userItem.user.s_number,
				  }
				: {
						s_number: 0,
				  }
		),
	});

	hide(userItem);
}

/**
 * Fetches all messages in a conversation using Tinder API
 * @param {string} matchId
 * @returns {Promise<any>}
 */
async function fetchMessages(matchId) {
	return fetch(`https://api.gotinder.com/v2/matches/${matchId}/messages?locale=en&count=100`, {
		headers: {
			'X-Auth-Token': localStorage.getItem('TinderWeb/APIToken') ?? '',
		},
	})
		.then((res) => res.json())
		.then((res) => res.data.messages);
}

/**
 * Fetches matches using Tinder API
 * @returns {Promise<any>}
 */
async function fetchMatches() {
	return fetch('https://api.gotinder.com/v2/matches?locale=en&count=60&message=1', {
		headers: {
			'X-Auth-Token': localStorage.getItem('TinderWeb/APIToken') ?? '',
		},
	})
		.then((res) => res.json())
		.then((res) => res.data.matches);
}

/**
 * Fetches teaser cards using Tinder API
 * @returns {Promise<any>}
 */
async function fetchTeasers() {
	return fetch('https://api.gotinder.com/v2/fast-match/teasers', {
		headers: {
			'X-Auth-Token': localStorage.getItem('TinderWeb/APIToken') ?? '',
			platform: 'android',
		},
	})
		.then((res) => res.json())
		.then((res) => res.data.results);
}

/**
 * Fetches information about specific user using Tinder API
 * @param {string} id
 * @returns {Promise<any>}
 */
async function fetchUser(id) {
	/* disabled due to API changes, currently looking for a workaround!

	return fetch(`https://api.gotinder.com/user/${id}`, {
		headers: {
			'X-Auth-Token': localStorage.getItem('TinderWeb/APIToken') ?? '',
			platform: 'android',
		},
	})
		.then((res) => res.json())
		.then((res) => res.results);*/

	return null;
}

/**
 * Awaits the first event of the specified listener
 * @param {EventTarget} target
 * @param {string} eventType
 * @returns {Promise<void>}
 */
async function once(target, eventType) {
	return new Promise((resolve) => {
		const resolver = () => {
			target.removeEventListener(eventType, resolver);
			resolve();
		};
		target.addEventListener(eventType, resolver);
	});
}

/**
 * Utility function to catch errors inline
 * @template T
 * @template {Error} U
 * @param {Promise<T>} promise
 * @return {Promise<[null, T] | [U, undefined]>}
 */
async function safeAwait(promise) {
	try {
		const result = await promise;
		return [null, result];
	} catch (err) {
		return [err, undefined];
	}
}

/**
 * Awaits until the main app element is found in DOM and returns it
 * @returns {Promise<HTMLElement>}
 */
async function waitForApp() {
	const getAppEl = (parent) => parent.querySelector('.App');
	let appEl = getAppEl(document.body);

	if (appEl) return appEl;

	return new Promise((resolve) => {
		new MutationObserver((_, me) => {
			appEl = getAppEl(document.body);

			if (appEl) {
				me.disconnect();
				resolve(appEl);
			}
		}).observe(document.body, { subtree: true, childList: true });
	});
}

async function main() {
	// check if running as a userscript
	if (typeof GM_info === 'undefined') {
		console.warn(
			'[TINDER DEBLUR]: The only supported way of running this script is through a userscript management browser addons like Violentmonkey, Tampermonkey or Greasemonkey!'
		);
		console.warn(
			'[TINDER DEBLUR]: Script was not terminated, but you should really look into the correct way of running it.'
		);
	}

	// wait for a full page load
	await once(window, 'load');
	const appEl = await waitForApp();

	const pageCheckCallback = async () => {
		if (['/app/likes-you', '/app/gold-home'].includes(location.pathname)) {
			// check if any likes were loaded yet
			if (document.querySelectorAll('div.focus-button-style[style]').length > 0) {
				console.debug('[TINDER DEBLUR]: Removing Tinder Gold ads');
				removeGoldAds();

				console.debug('[TINDER DEBLUR]: Checking filters');
				updateUserFiltering();

				console.debug('[TINDER DEBLUR]: Deblurring likes');
				await unblur();
			}

			console.debug('[TINDER DEBLUR]: Updating user infos');
			updateUserInfos();
		} else {
			// clear the cache when not on likes page anymore
			cache.clear();

			if (location.pathname.startsWith('/app/messages/')) {
				console.debug('[TINDER DEBLUR]: Updating message infos');
				updateMessageInfos(location.pathname.substring(location.pathname.lastIndexOf('/') + 1));
			}
		}

		// loop based observer (every 4s)
		setTimeout(pageCheckCallback, 4_000);
	};

	pageCheckCallback();
}

main().catch(console.error);