__filename__ = "webapp_profile.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
__version__ = "1.2.0"
__maintainer__ = "Bob Mottram"
__email__ = "bob@freedombone.net"
__status__ = "Production"
__module_group__ = "Web Interface"

import os
from pprint import pprint
from utils import hasObjectDict
from utils import getOccupationName
from utils import getLockedAccount
from utils import getFullDomain
from utils import isArtist
from utils import isDormant
from utils import getNicknameFromActor
from utils import getDomainFromActor
from utils import isSystemAccount
from utils import removeHtml
from utils import loadJson
from utils import getConfigParam
from utils import getImageFormats
from utils import acctDir
from languages import getActorLanguages
from skills import getSkills
from theme import getThemesList
from person import personBoxJson
from person import getActorJson
from person import getPersonAvatarUrl
from webfinger import webfingerHandle
from posts import parseUserFeed
from posts import getPersonBox
from donate import getDonationUrl
from xmpp import getXmppAddress
from matrix import getMatrixAddress
from ssb import getSSBAddress
from pgp import getEmailAddress
from pgp import getPGPfingerprint
from pgp import getPGPpubKey
from tox import getToxAddress
from briar import getBriarAddress
from jami import getJamiAddress
from cwtch import getCwtchAddress
from filters import isFiltered
from follow import isFollowerOfPerson
from webapp_frontscreen import htmlFrontScreen
from webapp_utils import htmlKeyboardNavigation
from webapp_utils import htmlHideFromScreenReader
from webapp_utils import scheduledPostsExist
from webapp_utils import htmlHeaderWithExternalStyle
from webapp_utils import htmlHeaderWithPersonMarkup
from webapp_utils import htmlFooter
from webapp_utils import addEmojiToDisplayName
from webapp_utils import getBannerFile
from webapp_utils import htmlPostSeparator
from webapp_utils import editCheckBox
from webapp_utils import editTextField
from webapp_utils import editTextArea
from webapp_utils import beginEditSection
from webapp_utils import endEditSection
from blog import getBlogAddress
from webapp_post import individualPostAsHtml
from webapp_timeline import htmlIndividualShare


def htmlProfileAfterSearch(cssCache: {},
                           recentPostsCache: {}, maxRecentPosts: int,
                           translate: {},
                           baseDir: str, path: str, httpPrefix: str,
                           nickname: str, domain: str, port: int,
                           profileHandle: str,
                           session, cachedWebfingers: {}, personCache: {},
                           debug: bool, projectVersion: str,
                           YTReplacementDomain: str,
                           showPublishedDateOnly: bool,
                           defaultTimeline: str,
                           peertubeInstances: [],
                           allowLocalNetworkAccess: bool,
                           themeName: str,
                           accessKeys: {},
                           systemLanguage: str) -> str:
    """Show a profile page after a search for a fediverse address
    """
    http = False
    gnunet = False
    if httpPrefix == 'http':
        http = True
    elif httpPrefix == 'gnunet':
        gnunet = True
    profileJson, asHeader = \
        getActorJson(domain, profileHandle, http, gnunet, debug, False)
    if not profileJson:
        return None

    personUrl = profileJson['id']
    searchDomain, searchPort = getDomainFromActor(personUrl)
    searchNickname = getNicknameFromActor(personUrl)
    searchDomainFull = getFullDomain(searchDomain, searchPort)

    profileStr = ''
    cssFilename = baseDir + '/epicyon-profile.css'
    if os.path.isfile(baseDir + '/epicyon.css'):
        cssFilename = baseDir + '/epicyon.css'

    avatarUrl = ''
    if profileJson.get('icon'):
        if profileJson['icon'].get('url'):
            avatarUrl = profileJson['icon']['url']
    if not avatarUrl:
        avatarUrl = getPersonAvatarUrl(baseDir, personUrl,
                                       personCache, True)
    displayName = searchNickname
    if profileJson.get('name'):
        displayName = profileJson['name']

    lockedAccount = getLockedAccount(profileJson)
    if lockedAccount:
        displayName += '🔒'
    movedTo = ''
    if profileJson.get('movedTo'):
        movedTo = profileJson['movedTo']
        displayName += ' ⌂'

    followsYou = \
        isFollowerOfPerson(baseDir,
                           nickname, domain,
                           searchNickname,
                           searchDomainFull)

    profileDescription = ''
    if profileJson.get('summary'):
        profileDescription = profileJson['summary']
    outboxUrl = None
    if not profileJson.get('outbox'):
        if debug:
            pprint(profileJson)
            print('DEBUG: No outbox found')
        return None
    outboxUrl = profileJson['outbox']

    # profileBackgroundImage = ''
    # if profileJson.get('image'):
    #     if profileJson['image'].get('url'):
    #         profileBackgroundImage = profileJson['image']['url']

    # url to return to
    backUrl = path
    if not backUrl.endswith('/inbox'):
        backUrl += '/inbox'

    profileDescriptionShort = profileDescription
    if '\n' in profileDescription:
        if len(profileDescription.split('\n')) > 2:
            profileDescriptionShort = ''
    else:
        if '<br>' in profileDescription:
            if len(profileDescription.split('<br>')) > 2:
                profileDescriptionShort = ''
    # keep the profile description short
    if len(profileDescriptionShort) > 256:
        profileDescriptionShort = ''
    # remove formatting from profile description used on title
    avatarDescription = ''
    if profileJson.get('summary'):
        if isinstance(profileJson['summary'], str):
            avatarDescription = \
                profileJson['summary'].replace('<br>', '\n')
            avatarDescription = avatarDescription.replace('<p>', '')
            avatarDescription = avatarDescription.replace('</p>', '')
            if '<' in avatarDescription:
                avatarDescription = removeHtml(avatarDescription)

    imageUrl = ''
    if profileJson.get('image'):
        if profileJson['image'].get('url'):
            imageUrl = profileJson['image']['url']

    alsoKnownAs = None
    if profileJson.get('alsoKnownAs'):
        alsoKnownAs = profileJson['alsoKnownAs']

    joinedDate = None
    if profileJson.get('published'):
        if 'T' in profileJson['published']:
            joinedDate = profileJson['published']

    profileStr = \
        _getProfileHeaderAfterSearch(baseDir,
                                     nickname, defaultTimeline,
                                     searchNickname,
                                     searchDomainFull,
                                     translate,
                                     displayName, followsYou,
                                     profileDescriptionShort,
                                     avatarUrl, imageUrl,
                                     movedTo, profileJson['id'],
                                     alsoKnownAs, accessKeys,
                                     joinedDate)

    domainFull = getFullDomain(domain, port)

    followIsPermitted = True
    if searchNickname == 'news' and searchDomainFull == domainFull:
        # currently the news actor is not something you can follow
        followIsPermitted = False
    elif searchNickname == nickname and searchDomainFull == domainFull:
        # don't follow yourself!
        followIsPermitted = False

    if followIsPermitted:
        profileStr += \
            '<div class="container">\n' + \
            '  <form method="POST" action="' + \
            backUrl + '/followconfirm">\n' + \
            '    <center>\n' + \
            '      <input type="hidden" name="actor" value="' + \
            personUrl + '">\n' + \
            '      <button type="submit" class="button" name="submitYes" ' + \
            'accesskey="' + accessKeys['followButton'] + '">' + \
            translate['Follow'] + '</button>\n' + \
            '      <button type="submit" class="button" name="submitView" ' + \
            'accesskey="' + accessKeys['viewButton'] + '">' + \
            translate['View'] + '</button>\n' + \
            '    </center>\n' + \
            '  </form>\n' + \
            '</div>\n'

    i = 0
    for item in parseUserFeed(session, outboxUrl, asHeader,
                              projectVersion, httpPrefix, domain):
        if not item.get('actor'):
            continue
        if item['actor'] != personUrl:
            continue
        if not item.get('type'):
            continue
        if item['type'] != 'Create':
            continue
        if not hasObjectDict(item):
            continue

        profileStr += \
            individualPostAsHtml(True, recentPostsCache, maxRecentPosts,
                                 translate, None, baseDir,
                                 session, cachedWebfingers, personCache,
                                 nickname, domain, port,
                                 item, avatarUrl, False, False,
                                 httpPrefix, projectVersion, 'inbox',
                                 YTReplacementDomain,
                                 showPublishedDateOnly,
                                 peertubeInstances, allowLocalNetworkAccess,
                                 themeName, systemLanguage,
                                 False, False, False, False, False)
        i += 1
        if i >= 20:
            break

    instanceTitle = \
        getConfigParam(baseDir, 'instanceTitle')
    return htmlHeaderWithExternalStyle(cssFilename, instanceTitle) + \
        profileStr + htmlFooter()


def _getProfileHeader(baseDir: str, httpPrefix: str,
                      nickname: str, domain: str,
                      domainFull: str, translate: {},
                      defaultTimeline: str,
                      displayName: str,
                      avatarDescription: str,
                      profileDescriptionShort: str,
                      loginButton: str, avatarUrl: str,
                      theme: str, movedTo: str,
                      alsoKnownAs: [],
                      pinnedContent: str,
                      accessKeys: {},
                      joinedDate: str,
                      occupationName: str) -> str:
    """The header of the profile screen, containing background
    image and avatar
    """
    htmlStr = \
        '\n\n    <figure class="profileHeader">\n' + \
        '      <a href="/users/' + \
        nickname + '/' + defaultTimeline + '" title="' + \
        translate['Switch to timeline view'] + '">\n' + \
        '        <img class="profileBackground" ' + \
        'alt="" ' + \
        'src="/users/' + nickname + '/image_' + theme + '.png" /></a>\n' + \
        '      <figcaption>\n' + \
        '        <a href="/users/' + \
        nickname + '/' + defaultTimeline + '" title="' + \
        translate['Switch to timeline view'] + '">\n' + \
        '          <img loading="lazy" src="' + avatarUrl + '" ' + \
        'alt=""  class="title"></a>\n'

    occupationStr = ''
    if occupationName:
        occupationStr += \
            '        <b>' + occupationName + '</b><br>\n'

    htmlStr += '        <h1>' + displayName + '</h1>\n' + occupationStr

    htmlStr += \
        '    <p><b>@' + nickname + '@' + domainFull + '</b><br>\n'
    if joinedDate:
        htmlStr += \
            '    <p>' + translate['Joined'] + ' ' + \
            joinedDate.split('T')[0] + '<br>\n'
    if movedTo:
        newNickname = getNicknameFromActor(movedTo)
        newDomain, newPort = getDomainFromActor(movedTo)
        newDomainFull = getFullDomain(newDomain, newPort)
        if newNickname and newDomain:
            htmlStr += \
                '    <p>' + translate['New account'] + ': ' + \
                '<a href="' + movedTo + '">@' + \
                newNickname + '@' + newDomainFull + '</a><br>\n'
    elif alsoKnownAs:
        otherAccountsHtml = \
            '    <p>' + translate['Other accounts'] + ': '

        actor = httpPrefix + '://' + domainFull + '/users/' + nickname
        ctr = 0
        if isinstance(alsoKnownAs, list):
            for altActor in alsoKnownAs:
                if altActor == actor:
                    continue
                if ctr > 0:
                    otherAccountsHtml += ' '
                ctr += 1
                altDomain, altPort = getDomainFromActor(altActor)
                otherAccountsHtml += \
                    '<a href="' + altActor + '">' + altDomain + '</a>'
        elif isinstance(alsoKnownAs, str):
            if alsoKnownAs != actor:
                ctr += 1
                altDomain, altPort = getDomainFromActor(alsoKnownAs)
                otherAccountsHtml += \
                    '<a href="' + alsoKnownAs + '">' + altDomain + '</a>'
        otherAccountsHtml += '</p>\n'
        if ctr > 0:
            htmlStr += otherAccountsHtml
    htmlStr += \
        '    <a href="/users/' + nickname + \
        '/qrcode.png" alt="' + translate['QR Code'] + '" title="' + \
        translate['QR Code'] + '">' + \
        '<img class="qrcode" alt="' + translate['QR Code'] + \
        '" src="/icons/qrcode.png" /></a></p>\n' + \
        '        <p>' + profileDescriptionShort + '</p>\n' + loginButton
    if pinnedContent:
        htmlStr += pinnedContent.replace('<p>', '<p>📎', 1)
    htmlStr += \
        '      </figcaption>\n' + \
        '    </figure>\n\n'
    return htmlStr


def _getProfileHeaderAfterSearch(baseDir: str,
                                 nickname: str, defaultTimeline: str,
                                 searchNickname: str,
                                 searchDomainFull: str,
                                 translate: {},
                                 displayName: str,
                                 followsYou: bool,
                                 profileDescriptionShort: str,
                                 avatarUrl: str, imageUrl: str,
                                 movedTo: str, actor: str,
                                 alsoKnownAs: [],
                                 accessKeys: {},
                                 joinedDate: str) -> str:
    """The header of a searched for handle, containing background
    image and avatar
    """
    htmlStr = \
        '\n\n    <figure class="profileHeader">\n' + \
        '      <a href="/users/' + \
        nickname + '/' + defaultTimeline + '" title="' + \
        translate['Switch to timeline view'] + '" ' + \
        'accesskey="' + accessKeys['menuTimeline'] + '">\n' + \
        '        <img class="profileBackground" ' + \
        'alt="" ' + \
        'src="' + imageUrl + '" /></a>\n' + \
        '      <figcaption>\n'
    if avatarUrl:
        htmlStr += \
            '      <a href="/users/' + \
            nickname + '/' + defaultTimeline + '" title="' + \
            translate['Switch to timeline view'] + '">\n' + \
            '          <img loading="lazy" src="' + avatarUrl + '" ' + \
            'alt="" class="title"></a>\n'
    htmlStr += \
        '        <h1>' + displayName + '</h1>\n' + \
        '    <p><b>@' + searchNickname + '@' + searchDomainFull + '</b><br>\n'
    if joinedDate:
        htmlStr += '        <p>' + translate['Joined'] + ' ' + \
            joinedDate.split('T')[0] + '</p>\n'
    if followsYou:
        htmlStr += '        <p><b>' + translate['Follows you'] + '</b></p>\n'
    if movedTo:
        newNickname = getNicknameFromActor(movedTo)
        newDomain, newPort = getDomainFromActor(movedTo)
        newDomainFull = getFullDomain(newDomain, newPort)
        if newNickname and newDomain:
            newHandle = newNickname + '@' + newDomainFull
            htmlStr += '        <p>' + translate['New account'] + \
                ': < a href="' + movedTo + '">@' + newHandle + '</a></p>\n'
    elif alsoKnownAs:
        otherAccountshtml = \
            '        <p>' + translate['Other accounts'] + ': '

        ctr = 0
        if isinstance(alsoKnownAs, list):
            for altActor in alsoKnownAs:
                if altActor == actor:
                    continue
                if ctr > 0:
                    otherAccountshtml += ' '
                ctr += 1
                altDomain, altPort = getDomainFromActor(altActor)
                otherAccountshtml += \
                    '<a href="' + altActor + '">' + altDomain + '</a>'
        elif isinstance(alsoKnownAs, str):
            if alsoKnownAs != actor:
                ctr += 1
                altDomain, altPort = getDomainFromActor(alsoKnownAs)
                otherAccountshtml += \
                    '<a href="' + alsoKnownAs + '">' + altDomain + '</a>'

        otherAccountshtml += '</p>\n'
        if ctr > 0:
            htmlStr += otherAccountshtml

    htmlStr += \
        '        <p>' + profileDescriptionShort + '</p>\n' + \
        '      </figcaption>\n' + \
        '    </figure>\n\n'
    return htmlStr


def htmlProfile(rssIconAtTop: bool,
                cssCache: {}, iconsAsButtons: bool,
                defaultTimeline: str,
                recentPostsCache: {}, maxRecentPosts: int,
                translate: {}, projectVersion: str,
                baseDir: str, httpPrefix: str, authorized: bool,
                profileJson: {}, selected: str,
                session, cachedWebfingers: {}, personCache: {},
                YTReplacementDomain: str,
                showPublishedDateOnly: bool,
                newswire: {}, theme: str, dormantMonths: int,
                peertubeInstances: [],
                allowLocalNetworkAccess: bool,
                textModeBanner: str,
                debug: bool, accessKeys: {}, city: str,
                systemLanguage: str,
                extraJson: {} = None, pageNumber: int = None,
                maxItemsPerPage: int = None) -> str:
    """Show the profile page as html
    """
    nickname = profileJson['preferredUsername']
    if not nickname:
        return ""
    if isSystemAccount(nickname):
        return htmlFrontScreen(rssIconAtTop,
                               cssCache, iconsAsButtons,
                               defaultTimeline,
                               recentPostsCache, maxRecentPosts,
                               translate, projectVersion,
                               baseDir, httpPrefix, authorized,
                               profileJson, selected,
                               session, cachedWebfingers, personCache,
                               YTReplacementDomain,
                               showPublishedDateOnly,
                               newswire, theme, extraJson,
                               allowLocalNetworkAccess, accessKeys,
                               systemLanguage,
                               pageNumber, maxItemsPerPage)

    domain, port = getDomainFromActor(profileJson['id'])
    if not domain:
        return ""
    displayName = \
        addEmojiToDisplayName(baseDir, httpPrefix,
                              nickname, domain,
                              profileJson['name'], True)
    domainFull = getFullDomain(domain, port)
    profileDescription = \
        addEmojiToDisplayName(baseDir, httpPrefix,
                              nickname, domain,
                              profileJson['summary'], False)
    postsButton = 'button'
    followingButton = 'button'
    followersButton = 'button'
    rolesButton = 'button'
    skillsButton = 'button'
    sharesButton = 'button'
    if selected == 'posts':
        postsButton = 'buttonselected'
    elif selected == 'following':
        followingButton = 'buttonselected'
    elif selected == 'followers':
        followersButton = 'buttonselected'
    elif selected == 'roles':
        rolesButton = 'buttonselected'
    elif selected == 'skills':
        skillsButton = 'buttonselected'
    elif selected == 'shares':
        sharesButton = 'buttonselected'
    loginButton = ''

    followApprovalsSection = ''
    followApprovals = False
    editProfileStr = ''
    logoutStr = ''
    actor = profileJson['id']
    usersPath = '/users/' + actor.split('/users/')[1]

    donateSection = ''
    donateUrl = getDonationUrl(profileJson)
    PGPpubKey = getPGPpubKey(profileJson)
    PGPfingerprint = getPGPfingerprint(profileJson)
    emailAddress = getEmailAddress(profileJson)
    xmppAddress = getXmppAddress(profileJson)
    matrixAddress = getMatrixAddress(profileJson)
    ssbAddress = getSSBAddress(profileJson)
    toxAddress = getToxAddress(profileJson)
    briarAddress = getBriarAddress(profileJson)
    jamiAddress = getJamiAddress(profileJson)
    cwtchAddress = getCwtchAddress(profileJson)
    if donateUrl or xmppAddress or matrixAddress or \
       ssbAddress or toxAddress or briarAddress or \
       jamiAddress or cwtchAddress or PGPpubKey or \
       PGPfingerprint or emailAddress:
        donateSection = '<div class="container">\n'
        donateSection += '  <center>\n'
        if donateUrl and not isSystemAccount(nickname):
            donateSection += \
                '    <p><a href="' + donateUrl + \
                '"><button class="donateButton">' + translate['Donate'] + \
                '</button></a></p>\n'
        if emailAddress:
            donateSection += \
                '<p>' + translate['Email'] + ': <a href="mailto:' + \
                emailAddress + '">' + emailAddress + '</a></p>\n'
        if xmppAddress:
            donateSection += \
                '<p>' + translate['XMPP'] + ': <a href="xmpp:' + \
                xmppAddress + '">' + xmppAddress + '</a></p>\n'
        if matrixAddress:
            donateSection += \
                '<p>' + translate['Matrix'] + ': ' + matrixAddress + '</p>\n'
        if ssbAddress:
            donateSection += \
                '<p>SSB: <label class="ssbaddr">' + \
                ssbAddress + '</label></p>\n'
        if toxAddress:
            donateSection += \
                '<p>Tox: <label class="toxaddr">' + \
                toxAddress + '</label></p>\n'
        if briarAddress:
            if briarAddress.startswith('briar://'):
                donateSection += \
                    '<p><label class="toxaddr">' + \
                    briarAddress + '</label></p>\n'
            else:
                donateSection += \
                    '<p>briar://<label class="toxaddr">' + \
                    briarAddress + '</label></p>\n'
        if jamiAddress:
            donateSection += \
                '<p>Jami: <label class="toxaddr">' + \
                jamiAddress + '</label></p>\n'
        if cwtchAddress:
            donateSection += \
                '<p>Cwtch: <label class="toxaddr">' + \
                cwtchAddress + '</label></p>\n'
        if PGPfingerprint:
            donateSection += \
                '<p class="pgp">PGP: ' + \
                PGPfingerprint.replace('\n', '<br>') + '</p>\n'
        if PGPpubKey:
            donateSection += \
                '<p class="pgp">' + PGPpubKey.replace('\n', '<br>') + '</p>\n'
        donateSection += '  </center>\n'
        donateSection += '</div>\n'

    if authorized:
        editProfileStr = \
            '<a class="imageAnchor" href="' + usersPath + '/editprofile">' + \
            '<img loading="lazy" src="/icons' + \
            '/edit.png" title="' + translate['Edit'] + \
            '" alt="| ' + translate['Edit'] + '" class="timelineicon"/></a>\n'

        logoutStr = \
            '<a class="imageAnchor" href="/logout">' + \
            '<img loading="lazy" src="/icons' + \
            '/logout.png" title="' + translate['Logout'] + \
            '" alt="| ' + translate['Logout'] + \
            '" class="timelineicon"/></a>\n'

        # are there any follow requests?
        followRequestsFilename = \
            acctDir(baseDir, nickname, domain) + '/followrequests.txt'
        if os.path.isfile(followRequestsFilename):
            with open(followRequestsFilename, 'r') as f:
                for line in f:
                    if len(line) > 0:
                        followApprovals = True
                        followersButton = 'buttonhighlighted'
                        if selected == 'followers':
                            followersButton = 'buttonselectedhighlighted'
                        break
        if selected == 'followers':
            if followApprovals:
                with open(followRequestsFilename, 'r') as f:
                    for followerHandle in f:
                        if len(line) > 0:
                            if '://' in followerHandle:
                                followerActor = followerHandle
                            else:
                                followerActor = \
                                    httpPrefix + '://' + \
                                    followerHandle.split('@')[1] + \
                                    '/users/' + followerHandle.split('@')[0]
                            basePath = '/users/' + nickname
                            followApprovalsSection += '<div class="container">'
                            followApprovalsSection += \
                                '<a href="' + followerActor + '">'
                            followApprovalsSection += \
                                '<span class="followRequestHandle">' + \
                                followerHandle + '</span></a>'
                            followApprovalsSection += \
                                '<a href="' + basePath + \
                                '/followapprove=' + followerHandle + '">'
                            followApprovalsSection += \
                                '<button class="followApprove">' + \
                                translate['Approve'] + '</button></a><br><br>'
                            followApprovalsSection += \
                                '<a href="' + basePath + \
                                '/followdeny=' + followerHandle + '">'
                            followApprovalsSection += \
                                '<button class="followDeny">' + \
                                translate['Deny'] + '</button></a>'
                            followApprovalsSection += '</div>'

    profileDescriptionShort = profileDescription
    if '\n' in profileDescription:
        if len(profileDescription.split('\n')) > 2:
            profileDescriptionShort = ''
    else:
        if '<br>' in profileDescription:
            if len(profileDescription.split('<br>')) > 2:
                profileDescriptionShort = ''
                profileDescription = profileDescription.replace('<br>', '\n')
    # keep the profile description short
    if len(profileDescriptionShort) > 256:
        profileDescriptionShort = ''
    # remove formatting from profile description used on title
    avatarDescription = ''
    if profileJson.get('summary'):
        avatarDescription = profileJson['summary'].replace('<br>', '\n')
        avatarDescription = avatarDescription.replace('<p>', '')
        avatarDescription = avatarDescription.replace('</p>', '')

    movedTo = ''
    if profileJson.get('movedTo'):
        movedTo = profileJson['movedTo']

    alsoKnownAs = None
    if profileJson.get('alsoKnownAs'):
        alsoKnownAs = profileJson['alsoKnownAs']

    joinedDate = None
    if profileJson.get('published'):
        if 'T' in profileJson['published']:
            joinedDate = profileJson['published']
    occupationName = None
    if profileJson.get('hasOccupation'):
        occupationName = getOccupationName(profileJson)

    avatarUrl = profileJson['icon']['url']
    # use alternate path for local avatars to avoid any caching issues
    if '://' + domainFull + '/accounts/avatars/' in avatarUrl:
        avatarUrl = \
            avatarUrl.replace('://' + domainFull + '/accounts/avatars/',
                              '://' + domainFull + '/users/')

    # get pinned post content
    accountDir = acctDir(baseDir, nickname, domain)
    pinnedFilename = accountDir + '/pinToProfile.txt'
    pinnedContent = None
    if os.path.isfile(pinnedFilename):
        with open(pinnedFilename, 'r') as pinFile:
            pinnedContent = pinFile.read()

    profileHeaderStr = \
        _getProfileHeader(baseDir, httpPrefix,
                          nickname, domain,
                          domainFull, translate,
                          defaultTimeline, displayName,
                          avatarDescription,
                          profileDescriptionShort,
                          loginButton, avatarUrl, theme,
                          movedTo, alsoKnownAs,
                          pinnedContent, accessKeys,
                          joinedDate, occupationName)

    # keyboard navigation
    userPathStr = '/users/' + nickname
    deft = defaultTimeline
    menuTimeline = \
        htmlHideFromScreenReader('🏠') + ' ' + \
        translate['Switch to timeline view']
    menuEdit = \
        htmlHideFromScreenReader('✍') + ' ' + translate['Edit']
    menuFollowing = \
        htmlHideFromScreenReader('👥') + ' ' + translate['Following']
    menuFollowers = \
        htmlHideFromScreenReader('👪') + ' ' + translate['Followers']
    menuRoles = \
        htmlHideFromScreenReader('🤚') + ' ' + translate['Roles']
    menuSkills = \
        htmlHideFromScreenReader('🛠') + ' ' + translate['Skills']
    menuShares = \
        htmlHideFromScreenReader('🤝') + ' ' + translate['Shares']
    menuLogout = \
        htmlHideFromScreenReader('❎') + ' ' + translate['Logout']
    navLinks = {
        menuTimeline: userPathStr + '/' + deft,
        menuEdit: userPathStr + '/editprofile',
        menuFollowing: userPathStr + '/following#timeline',
        menuFollowers: userPathStr + '/followers#timeline',
        menuRoles: userPathStr + '/roles#timeline',
        menuSkills: userPathStr + '/skills#timeline',
        menuShares: userPathStr + '/shares#timeline',
        menuLogout: '/logout'
    }
    navAccessKeys = {}
    for variableName, key in accessKeys.items():
        if not locals().get(variableName):
            continue
        navAccessKeys[locals()[variableName]] = key

    profileStr = htmlKeyboardNavigation(textModeBanner,
                                        navLinks, navAccessKeys)

    profileStr += profileHeaderStr + donateSection
    profileStr += '<div class="container" id="buttonheader">\n'
    profileStr += '  <center>'
    profileStr += \
        '    <a href="' + usersPath + '#buttonheader"><button class="' + \
        postsButton + '"><span>' + translate['Posts'] + \
        ' </span></button></a>'
    profileStr += \
        '    <a href="' + usersPath + '/following#buttonheader">' + \
        '<button class="' + followingButton + '"><span>' + \
        translate['Following'] + ' </span></button></a>'
    profileStr += \
        '    <a href="' + usersPath + '/followers#buttonheader">' + \
        '<button class="' + followersButton + \
        '"><span>' + translate['Followers'] + ' </span></button></a>'
    profileStr += \
        '    <a href="' + usersPath + '/roles#buttonheader">' + \
        '<button class="' + rolesButton + '"><span>' + \
        translate['Roles'] + \
        ' </span></button></a>'
    profileStr += \
        '    <a href="' + usersPath + '/skills#buttonheader">' + \
        '<button class="' + skillsButton + '"><span>' + \
        translate['Skills'] + ' </span></button></a>'
    profileStr += \
        '    <a href="' + usersPath + '/shares#buttonheader">' + \
        '<button class="' + sharesButton + '"><span>' + \
        translate['Shares'] + ' </span></button></a>'
    profileStr += logoutStr + editProfileStr
    profileStr += '  </center>'
    profileStr += '</div>'

    # start of #timeline
    profileStr += '<div id="timeline">\n'

    profileStr += followApprovalsSection

    cssFilename = baseDir + '/epicyon-profile.css'
    if os.path.isfile(baseDir + '/epicyon.css'):
        cssFilename = baseDir + '/epicyon.css'

    licenseStr = \
        '<a href="https://gitlab.com/bashrc2/epicyon">' + \
        '<img loading="lazy" class="license" alt="' + \
        translate['Get the source code'] + '" title="' + \
        translate['Get the source code'] + '" src="/icons/agpl.png" /></a>'

    if selected == 'posts':
        profileStr += \
            _htmlProfilePosts(recentPostsCache, maxRecentPosts,
                              translate,
                              baseDir, httpPrefix, authorized,
                              nickname, domain, port,
                              session, cachedWebfingers, personCache,
                              projectVersion,
                              YTReplacementDomain,
                              showPublishedDateOnly,
                              peertubeInstances,
                              allowLocalNetworkAccess,
                              theme, systemLanguage) + licenseStr
    elif selected == 'following':
        profileStr += \
            _htmlProfileFollowing(translate, baseDir, httpPrefix,
                                  authorized, nickname,
                                  domain, port, session,
                                  cachedWebfingers, personCache, extraJson,
                                  projectVersion, ["unfollow"], selected,
                                  usersPath, pageNumber, maxItemsPerPage,
                                  dormantMonths, debug)
    elif selected == 'followers':
        profileStr += \
            _htmlProfileFollowing(translate, baseDir, httpPrefix,
                                  authorized, nickname,
                                  domain, port, session,
                                  cachedWebfingers, personCache, extraJson,
                                  projectVersion, ["block"],
                                  selected, usersPath, pageNumber,
                                  maxItemsPerPage, dormantMonths, debug)
    elif selected == 'roles':
        profileStr += \
            _htmlProfileRoles(translate, nickname, domainFull,
                              extraJson)
    elif selected == 'skills':
        profileStr += \
            _htmlProfileSkills(translate, nickname, domainFull, extraJson)
    elif selected == 'shares':
        profileStr += \
            _htmlProfileShares(actor, translate,
                               nickname, domainFull,
                               extraJson) + licenseStr
    # end of #timeline
    profileStr += '</div>'

    instanceTitle = \
        getConfigParam(baseDir, 'instanceTitle')
    profileStr = \
        htmlHeaderWithPersonMarkup(cssFilename, instanceTitle,
                                   profileJson, city) + \
        profileStr + htmlFooter()
    return profileStr


def _htmlProfilePosts(recentPostsCache: {}, maxRecentPosts: int,
                      translate: {},
                      baseDir: str, httpPrefix: str,
                      authorized: bool,
                      nickname: str, domain: str, port: int,
                      session, cachedWebfingers: {}, personCache: {},
                      projectVersion: str,
                      YTReplacementDomain: str,
                      showPublishedDateOnly: bool,
                      peertubeInstances: [],
                      allowLocalNetworkAccess: bool,
                      themeName: str, systemLanguage: str) -> str:
    """Shows posts on the profile screen
    These should only be public posts
    """
    separatorStr = htmlPostSeparator(baseDir, None)
    profileStr = ''
    maxItems = 4
    ctr = 0
    currPage = 1
    boxName = 'outbox'
    while ctr < maxItems and currPage < 4:
        outboxFeedPathStr = \
            '/users/' + nickname + '/' + boxName + '?page=' + \
            str(currPage)
        outboxFeed = \
            personBoxJson({}, session, baseDir, domain,
                          port,
                          outboxFeedPathStr,
                          httpPrefix,
                          10, boxName,
                          authorized, 0, False, 0)
        if not outboxFeed:
            break
        if len(outboxFeed['orderedItems']) == 0:
            break
        for item in outboxFeed['orderedItems']:
            if item['type'] == 'Create':
                postStr = \
                    individualPostAsHtml(True, recentPostsCache,
                                         maxRecentPosts,
                                         translate, None,
                                         baseDir, session, cachedWebfingers,
                                         personCache,
                                         nickname, domain, port, item,
                                         None, True, False,
                                         httpPrefix, projectVersion, 'inbox',
                                         YTReplacementDomain,
                                         showPublishedDateOnly,
                                         peertubeInstances,
                                         allowLocalNetworkAccess,
                                         themeName, systemLanguage,
                                         False, False, False, True, False)
                if postStr:
                    profileStr += postStr + separatorStr
                    ctr += 1
                    if ctr >= maxItems:
                        break
        currPage += 1
    return profileStr


def _htmlProfileFollowing(translate: {}, baseDir: str, httpPrefix: str,
                          authorized: bool,
                          nickname: str, domain: str, port: int,
                          session, cachedWebfingers: {}, personCache: {},
                          followingJson: {}, projectVersion: str,
                          buttons: [],
                          feedName: str, actor: str,
                          pageNumber: int,
                          maxItemsPerPage: int,
                          dormantMonths: int, debug: bool) -> str:
    """Shows following on the profile screen
    """
    profileStr = ''

    if authorized and pageNumber:
        if authorized and pageNumber > 1:
            # page up arrow
            profileStr += \
                '  <center>\n' + \
                '    <a href="' + actor + '/' + feedName + \
                '?page=' + str(pageNumber - 1) + '#buttonheader' + \
                '"><img loading="lazy" class="pageicon" src="/' + \
                'icons/pageup.png" title="' + \
                translate['Page up'] + '" alt="' + \
                translate['Page up'] + '"></a>\n' + \
                '  </center>\n'

    for followingActor in followingJson['orderedItems']:
        # is this a dormant followed account?
        dormant = False
        if authorized and feedName == 'following':
            dormant = \
                isDormant(baseDir, nickname, domain, followingActor,
                          dormantMonths)

        profileStr += \
            _individualFollowAsHtml(translate, baseDir, session,
                                    cachedWebfingers, personCache,
                                    domain, followingActor,
                                    authorized, nickname,
                                    httpPrefix, projectVersion, dormant,
                                    debug, buttons)

    if authorized and maxItemsPerPage and pageNumber:
        if len(followingJson['orderedItems']) >= maxItemsPerPage:
            # page down arrow
            profileStr += \
                '  <center>\n' + \
                '    <a href="' + actor + '/' + feedName + \
                '?page=' + str(pageNumber + 1) + '#buttonheader' + \
                '"><img loading="lazy" class="pageicon" src="/' + \
                'icons/pagedown.png" title="' + \
                translate['Page down'] + '" alt="' + \
                translate['Page down'] + '"></a>\n' + \
                '  </center>\n'
    return profileStr


def _htmlProfileRoles(translate: {}, nickname: str, domain: str,
                      rolesList: []) -> str:
    """Shows roles on the profile screen
    """
    profileStr = ''
    profileStr += \
        '<div class="roles">\n<div class="roles-inner">\n'
    for role in rolesList:
        if translate.get(role):
            profileStr += '<h3>' + translate[role] + '</h3>\n'
        else:
            profileStr += '<h3>' + role + '</h3>\n'
    profileStr += '</div></div>\n'
    if len(profileStr) == 0:
        profileStr += \
            '<p>@' + nickname + '@' + domain + ' has no roles assigned</p>\n'
    else:
        profileStr = '<div>' + profileStr + '</div>\n'
    return profileStr


def _htmlProfileSkills(translate: {}, nickname: str, domain: str,
                       skillsJson: {}) -> str:
    """Shows skills on the profile screen
    """
    profileStr = ''
    for skill, level in skillsJson.items():
        profileStr += \
            '<div>' + skill + \
            '<br><div id="myProgress"><div id="myBar" style="width:' + \
            str(level) + '%"></div></div></div>\n<br>\n'
    if len(profileStr) > 0:
        profileStr = '<center><div class="skill-title">' + \
            profileStr + '</div></center>\n'
    return profileStr


def _htmlProfileShares(actor: str, translate: {},
                       nickname: str, domain: str, sharesJson: {}) -> str:
    """Shows shares on the profile screen
    """
    profileStr = ''
    for item in sharesJson['orderedItems']:
        profileStr += htmlIndividualShare(actor, item, translate, False, False)
    if len(profileStr) > 0:
        profileStr = '<div class="share-title">' + profileStr + '</div>\n'
    return profileStr


def _grayscaleEnabled(baseDir: str) -> bool:
    """Is grayscale UI enabled?
    """
    return os.path.isfile(baseDir + '/accounts/.grayscale')


def _htmlThemesDropdown(baseDir: str, translate: {}) -> str:
    """Returns the html for theme selection dropdown
    """
    # Themes section
    themes = getThemesList(baseDir)
    themesDropdown = '  <label class="labels">' + \
        translate['Theme'] + '</label><br>\n'
    grayscale = _grayscaleEnabled(baseDir)
    themesDropdown += \
        editCheckBox(translate['Grayscale'], 'grayscale', grayscale)
    themesDropdown += '  <select id="themeDropdown" ' + \
        'name="themeDropdown" class="theme">'
    for themeName in themes:
        translatedThemeName = themeName
        if translate.get(themeName):
            translatedThemeName = translate[themeName]
        themesDropdown += '    <option value="' + \
            themeName.lower() + '">' + \
            translatedThemeName + '</option>'
    themesDropdown += '  </select><br>'
    if os.path.isfile(baseDir + '/fonts/custom.woff') or \
       os.path.isfile(baseDir + '/fonts/custom.woff2') or \
       os.path.isfile(baseDir + '/fonts/custom.otf') or \
       os.path.isfile(baseDir + '/fonts/custom.ttf'):
        themesDropdown += \
            editCheckBox(translate['Remove the custom font'],
                         'removeCustomFont', False)
    themeName = getConfigParam(baseDir, 'theme')
    themesDropdown = \
        themesDropdown.replace('<option value="' + themeName + '">',
                               '<option value="' + themeName +
                               '" selected>')
    return themesDropdown


def _htmlEditProfileGraphicDesign(baseDir: str, translate: {}) -> str:
    """Graphic design section on Edit Profile screen
    """
    themeFormats = '.zip, .gz'

    graphicsStr = beginEditSection(translate['Graphic Design'])

    graphicsStr += _htmlThemesDropdown(baseDir, translate)
    graphicsStr += \
        '      <label class="labels">' + \
        translate['Import Theme'] + '</label>\n'
    graphicsStr += '      <input type="file" id="importTheme" '
    graphicsStr += 'name="submitImportTheme" '
    graphicsStr += 'accept="' + themeFormats + '">\n'
    graphicsStr += \
        '      <label class="labels">' + \
        translate['Export Theme'] + '</label><br>\n'
    graphicsStr += \
        '      <button type="submit" class="button" ' + \
        'name="submitExportTheme">➤</button>\n'

    graphicsStr += endEditSection()
    return graphicsStr


def _htmlEditProfileInstance(baseDir: str, translate: {},
                             peertubeInstances: [],
                             mediaInstanceStr: str,
                             blogsInstanceStr: str,
                             newsInstanceStr: str) -> (str, str, str, str):
    """Edit profile instance settings
    """
    imageFormats = getImageFormats()

    # Instance details section
    instanceDescription = \
        getConfigParam(baseDir, 'instanceDescription')
    customSubmitText = \
        getConfigParam(baseDir, 'customSubmitText')
    instanceDescriptionShort = \
        getConfigParam(baseDir, 'instanceDescriptionShort')
    instanceTitle = \
        getConfigParam(baseDir, 'instanceTitle')

    instanceStr = beginEditSection(translate['Instance Settings'])

    instanceStr += \
        editTextField(translate['Instance Title'],
                      'instanceTitle', instanceTitle)
    instanceStr += '<br>\n'
    instanceStr += \
        editTextField(translate['Instance Short Description'],
                      'instanceDescriptionShort', instanceDescriptionShort)
    instanceStr += '<br>\n'
    instanceStr += \
        editTextArea(translate['Instance Description'],
                     'instanceDescription', instanceDescription, 200,
                     '', True)
    instanceStr += \
        editTextField(translate['Custom post submit button text'],
                      'customSubmitText', customSubmitText)
    instanceStr += '<br>\n'
    instanceStr += \
        '  <label class="labels">' + \
        translate['Instance Logo'] + '</label>' + \
        '  <input type="file" id="instanceLogo" name="instanceLogo"' + \
        '      accept="' + imageFormats + '"><br>\n' + \
        '  <br><label class="labels">' + \
        translate['Security'] + '</label><br>\n'

    nodeInfoStr = \
        translate['Show numbers of accounts within instance metadata']
    if getConfigParam(baseDir, "showNodeInfoAccounts"):
        instanceStr += \
            editCheckBox(nodeInfoStr, 'showNodeInfoAccounts', True)
    else:
        instanceStr += \
            editCheckBox(nodeInfoStr, 'showNodeInfoAccounts', False)

    nodeInfoStr = \
        translate['Show version number within instance metadata']
    if getConfigParam(baseDir, "showNodeInfoVersion"):
        instanceStr += \
            editCheckBox(nodeInfoStr, 'showNodeInfoVersion', True)
    else:
        instanceStr += \
            editCheckBox(nodeInfoStr, 'showNodeInfoVersion', False)

    if getConfigParam(baseDir, "verifyAllSignatures"):
        instanceStr += \
            editCheckBox(translate['Verify all signatures'],
                         'verifyallsignatures', True)
    else:
        instanceStr += \
            editCheckBox(translate['Verify all signatures'],
                         'verifyallsignatures', False)

    instanceStr += translate['Enabling broch mode'] + '<br>\n'
    if getConfigParam(baseDir, "brochMode"):
        instanceStr += \
            editCheckBox(translate['Broch mode'], 'brochMode', True)
    else:
        instanceStr += \
            editCheckBox(translate['Broch mode'], 'brochMode', False)
    # Instance type
    instanceStr += \
        '  <br><label class="labels">' + \
        translate['Type of instance'] + '</label><br>\n'
    instanceStr += \
        editCheckBox(translate['This is a media instance'],
                     'mediaInstance', mediaInstanceStr)
    instanceStr += \
        editCheckBox(translate['This is a blogging instance'],
                     'blogsInstance', blogsInstanceStr)
    instanceStr += \
        editCheckBox(translate['This is a news instance'],
                     'newsInstance', newsInstanceStr)

    instanceStr += endEditSection()

    # Role assignments section
    moderators = ''
    moderatorsFile = baseDir + '/accounts/moderators.txt'
    if os.path.isfile(moderatorsFile):
        with open(moderatorsFile, 'r') as f:
            moderators = f.read()
    # site moderators
    roleAssignStr = \
        beginEditSection(translate['Role Assignment']) + \
        '  <b><label class="labels">' + \
        translate['Moderators'] + '</label></b><br>\n' + \
        '  ' + \
        translate['A list of moderator nicknames. One per line.'] + \
        '  <textarea id="message" name="moderators" placeholder="' + \
        translate['List of moderator nicknames'] + \
        '..." style="height:200px" spellcheck="false">' + \
        moderators + '</textarea>'

    # site editors
    editors = ''
    editorsFile = baseDir + '/accounts/editors.txt'
    if os.path.isfile(editorsFile):
        with open(editorsFile, 'r') as f:
            editors = f.read()
    roleAssignStr += \
        '  <b><label class="labels">' + \
        translate['Site Editors'] + '</label></b><br>\n' + \
        '  ' + \
        translate['A list of editor nicknames. One per line.'] + \
        '  <textarea id="message" name="editors" placeholder="" ' + \
        'style="height:200px" spellcheck="false">' + \
        editors + '</textarea>'

    # counselors
    counselors = ''
    counselorsFile = baseDir + '/accounts/counselors.txt'
    if os.path.isfile(counselorsFile):
        with open(counselorsFile, 'r') as f:
            counselors = f.read()
    roleAssignStr += \
        '  <b>' + \
        editTextArea(translate['Counselors'], 'counselors', counselors,
                     200, '', False)

    # artists
    artists = ''
    artistsFile = baseDir + '/accounts/artists.txt'
    if os.path.isfile(artistsFile):
        with open(artistsFile, 'r') as f:
            artists = f.read()
    roleAssignStr += \
        '  <b>' + \
        editTextArea(translate['Artists'], 'artists', artists,
                     200, '', False)
    roleAssignStr += endEditSection()

    # Video section
    peertubeStr = beginEditSection(translate['Video Settings'])
    peertubeInstancesStr = ''
    for url in peertubeInstances:
        peertubeInstancesStr += url + '\n'
    peertubeStr += \
        editTextArea(translate['Peertube Instances'], 'ptInstances',
                     peertubeInstancesStr, 200, '', False)
    peertubeStr += \
        '      <br>\n'
    YTReplacementDomain = getConfigParam(baseDir, "youtubedomain")
    if not YTReplacementDomain:
        YTReplacementDomain = ''
    peertubeStr += \
        editTextField(translate['YouTube Replacement Domain'],
                      'ytdomain', YTReplacementDomain)
    peertubeStr += endEditSection()

    libretranslateUrl = getConfigParam(baseDir, 'libretranslateUrl')
    libretranslateApiKey = getConfigParam(baseDir, 'libretranslateApiKey')
    libretranslateStr = \
        _htmlEditProfileLibreTranslate(translate,
                                       libretranslateUrl,
                                       libretranslateApiKey)

    return instanceStr, roleAssignStr, peertubeStr, libretranslateStr


def _htmlEditProfileDangerZone(translate: {}) -> str:
    """danger zone section of Edit Profile screen
    """
    editProfileForm = beginEditSection(translate['Danger Zone'])

    editProfileForm += \
        '      <b><label class="labels">' + \
        translate['Danger Zone'] + '</label></b><br>\n'

    editProfileForm += \
        editCheckBox(translate['Deactivate this account'],
                     'deactivateThisAccount', False)

    editProfileForm += endEditSection()
    return editProfileForm


def _htmlEditProfileSkills(baseDir: str, nickname: str, domain: str,
                           translate: {}) -> str:
    """skills section of Edit Profile screen
    """
    skills = getSkills(baseDir, nickname, domain)
    skillsStr = ''
    skillCtr = 1
    if skills:
        for skillDesc, skillValue in skills.items():
            if isFiltered(baseDir, nickname, domain, skillDesc):
                continue
            skillsStr += \
                '<p><input type="text" placeholder="' + translate['Skill'] + \
                ' ' + str(skillCtr) + '" name="skillName' + str(skillCtr) + \
                '" value="' + skillDesc + '" style="width:40%">' + \
                '<input type="range" min="1" max="100" ' + \
                'class="slider" name="skillValue' + \
                str(skillCtr) + '" value="' + str(skillValue) + '"></p>'
            skillCtr += 1

    skillsStr += \
        '<p><input type="text" placeholder="Skill ' + str(skillCtr) + \
        '" name="skillName' + str(skillCtr) + \
        '" value="" style="width:40%">' + \
        '<input type="range" min="1" max="100" ' + \
        'class="slider" name="skillValue' + \
        str(skillCtr) + '" value="50"></p>' + endEditSection()

    idx = 'If you want to participate within organizations then you ' + \
        'can indicate some skills that you have and approximate ' + \
        'proficiency levels. This helps organizers to construct ' + \
        'teams with an appropriate combination of skills.'
    editProfileForm = \
        beginEditSection(translate['Skills']) + \
        '      <b><label class="labels">' + \
        translate['Skills'] + '</label></b><br>\n' + \
        '      <label class="labels">' + \
        translate[idx] + '</label>\n' + skillsStr
    return editProfileForm


def _htmlEditProfileGitProjects(baseDir: str, nickname: str, domain: str,
                                translate: {}) -> str:
    """git projects section of edit profile screen
    """
    gitProjectsStr = ''
    gitProjectsFilename = \
        acctDir(baseDir, nickname, domain) + '/gitprojects.txt'
    if os.path.isfile(gitProjectsFilename):
        with open(gitProjectsFilename, 'r') as gitProjectsFile:
            gitProjectsStr = gitProjectsFile.read()

    editProfileForm = beginEditSection(translate['Git Projects'])
    idx = 'List of project names that you wish to receive git patches for'
    editProfileForm += \
        editTextArea(translate[idx], 'gitProjects', gitProjectsStr,
                     100, '', False)
    editProfileForm += endEditSection()
    return editProfileForm


def _htmlEditProfileFiltering(baseDir: str, nickname: str, domain: str,
                              userAgentsBlocked: str, translate: {}) -> str:
    """Filtering and blocking section of edit profile screen
    """
    filterStr = ''
    filterFilename = \
        acctDir(baseDir, nickname, domain) + '/filters.txt'
    if os.path.isfile(filterFilename):
        with open(filterFilename, 'r') as filterfile:
            filterStr = filterfile.read()

    switchStr = ''
    switchFilename = \
        acctDir(baseDir, nickname, domain) + '/replacewords.txt'
    if os.path.isfile(switchFilename):
        with open(switchFilename, 'r') as switchfile:
            switchStr = switchfile.read()

    autoTags = ''
    autoTagsFilename = \
        acctDir(baseDir, nickname, domain) + '/autotags.txt'
    if os.path.isfile(autoTagsFilename):
        with open(autoTagsFilename, 'r') as autoTagsFile:
            autoTags = autoTagsFile.read()

    autoCW = ''
    autoCWFilename = \
        acctDir(baseDir, nickname, domain) + '/autocw.txt'
    if os.path.isfile(autoCWFilename):
        with open(autoCWFilename, 'r') as autoCWFile:
            autoCW = autoCWFile.read()

    blockedStr = ''
    blockedFilename = \
        acctDir(baseDir, nickname, domain) + '/blocking.txt'
    if os.path.isfile(blockedFilename):
        with open(blockedFilename, 'r') as blockedfile:
            blockedStr = blockedfile.read()

    dmAllowedInstancesStr = ''
    dmAllowedInstancesFilename = \
        acctDir(baseDir, nickname, domain) + '/dmAllowedInstances.txt'
    if os.path.isfile(dmAllowedInstancesFilename):
        with open(dmAllowedInstancesFilename, 'r') as dmAllowedInstancesFile:
            dmAllowedInstancesStr = dmAllowedInstancesFile.read()

    allowedInstancesStr = ''
    allowedInstancesFilename = \
        acctDir(baseDir, nickname, domain) + '/allowedinstances.txt'
    if os.path.isfile(allowedInstancesFilename):
        with open(allowedInstancesFilename, 'r') as allowedInstancesFile:
            allowedInstancesStr = allowedInstancesFile.read()

    editProfileForm = beginEditSection(translate['Filtering and Blocking'])

    editProfileForm += \
        '<label class="labels">' + \
        translate['City for spoofed GPS image metadata'] + \
        '</label><br>\n'

    city = ''
    cityFilename = acctDir(baseDir, nickname, domain) + '/city.txt'
    if os.path.isfile(cityFilename):
        with open(cityFilename, 'r') as fp:
            city = fp.read().replace('\n', '')
    locationsFilename = baseDir + '/custom_locations.txt'
    if not os.path.isfile(locationsFilename):
        locationsFilename = baseDir + '/locations.txt'
    cities = []
    with open(locationsFilename, 'r') as f:
        cities = f.readlines()
        cities.sort()
    editProfileForm += '  <select id="cityDropdown" ' + \
        'name="cityDropdown" class="theme">\n'
    city = city.lower()
    for cityName in cities:
        if ':' not in cityName:
            continue
        citySelected = ''
        cityName = cityName.split(':')[0]
        cityName = cityName.lower()
        if city:
            if city in cityName:
                citySelected = ' selected'
        editProfileForm += \
            '    <option value="' + cityName + \
            '"' + citySelected.title() + '>' + \
            cityName + '</option>\n'
    editProfileForm += '  </select><br>\n'

    editProfileForm += \
        '      <b><label class="labels">' + \
        translate['Filtered words'] + '</label></b>\n' + \
        '      <br><label class="labels">' + \
        translate['One per line'] + '</label>\n' + \
        '      <textarea id="message" ' + \
        'name="filteredWords" style="height:200px" spellcheck="false">' + \
        filterStr + '</textarea>\n' + \
        '      <br><b><label class="labels">' + \
        translate['Word Replacements'] + '</label></b>\n' + \
        '      <br><label class="labels">A -> B</label>\n' + \
        '      <textarea id="message" name="switchWords" ' + \
        'style="height:200px" spellcheck="false">' + \
        switchStr + '</textarea>\n' + \
        '      <br><b><label class="labels">' + \
        translate['Autogenerated Hashtags'] + '</label></b>\n' + \
        '      <br><label class="labels">A -> #B</label>\n' + \
        '      <textarea id="message" name="autoTags" ' + \
        'style="height:200px" spellcheck="false">' + \
        autoTags + '</textarea>\n' + \
        '      <br><b><label class="labels">' + \
        translate['Autogenerated Content Warnings'] + '</label></b>\n' + \
        '      <br><label class="labels">A -> B</label>\n' + \
        '      <textarea id="message" name="autoCW" ' + \
        'style="height:200px" spellcheck="true">' + autoCW + '</textarea>\n'

    idx = 'Blocked accounts, one per line, in the form ' + \
        'nickname@domain or *@blockeddomain'
    editProfileForm += \
        editTextArea(translate['Blocked accounts'], 'blocked', blockedStr,
                     200, '', False)

    idx = 'Direct messages are always allowed from these instances.'
    editProfileForm += \
        editTextArea(translate['Direct Message permitted instances'],
                     'dmAllowedInstances', dmAllowedInstancesStr,
                     200, '', False)

    idx = 'Federate only with a defined set of instances. ' + \
        'One domain name per line.'
    editProfileForm += \
        '      <br><b><label class="labels">' + \
        translate['Federation list'] + '</label></b>\n' + \
        '      <br><label class="labels">' + \
        translate[idx] + '</label>\n' + \
        '      <textarea id="message" name="allowedInstances" ' + \
        'style="height:200px" spellcheck="false">' + \
        allowedInstancesStr + '</textarea>\n'

    userAgentsBlockedStr = ''
    for ua in userAgentsBlocked:
        if userAgentsBlockedStr:
            userAgentsBlockedStr += '\n'
        userAgentsBlockedStr += ua
    editProfileForm += \
        editTextArea(translate['Blocked User Agents'],
                     'userAgentsBlockedStr', userAgentsBlockedStr,
                     200, '', False)

    editProfileForm += endEditSection()
    return editProfileForm


def _htmlEditProfileChangePassword(translate: {}) -> str:
    """Change password section of edit profile screen
    """
    editProfileForm = \
        beginEditSection(translate['Change Password']) + \
        '<label class="labels">' + translate['Change Password'] + \
        '</label><br>\n' + \
        '      <input type="password" name="password" ' + \
        'value=""><br>\n' + \
        '<label class="labels">' + translate['Confirm Password'] + \
        '</label><br>\n' + \
        '      <input type="password" name="passwordconfirm" value="">\n' + \
        endEditSection()
    return editProfileForm


def _htmlEditProfileLibreTranslate(translate: {},
                                   libretranslateUrl: str,
                                   libretranslateApiKey: str) -> str:
    """Change automatic translation settings
    """
    editProfileForm = beginEditSection('LibreTranslate')

    editProfileForm += \
        editTextField('URL', 'libretranslateUrl', libretranslateUrl,
                      'http://0.0.0.0:5000')
    editProfileForm += \
        editTextField('API Key', 'libretranslateApiKey', libretranslateApiKey)

    editProfileForm += endEditSection()
    return editProfileForm


def _htmlEditProfileBackground(newsInstance: bool, translate: {}) -> str:
    """Background images section of edit profile screen
    """
    idx = 'The files attached below should be no larger than ' + \
        '10MB in total uploaded at once.'
    editProfileForm = \
        beginEditSection(translate['Background Images']) + \
        '      <label class="labels">' + translate[idx] + '</label><br><br>\n'

    if not newsInstance:
        imageFormats = getImageFormats()
        editProfileForm += \
            '      <label class="labels">' + \
            translate['Background image'] + '</label>\n' + \
            '      <input type="file" id="image" name="image"' + \
            '            accept="' + imageFormats + '">\n' + \
            '      <br><label class="labels">' + \
            translate['Timeline banner image'] + '</label>\n' + \
            '      <input type="file" id="banner" name="banner"' + \
            '            accept="' + imageFormats + '">\n' + \
            '      <br><label class="labels">' + \
            translate['Search banner image'] + '</label>\n' + \
            '      <input type="file" id="search_banner" ' + \
            'name="search_banner"' + \
            '            accept="' + imageFormats + '">\n' + \
            '      <br><label class="labels">' + \
            translate['Left column image'] + '</label>\n' + \
            '      <input type="file" id="left_col_image" ' + \
            'name="left_col_image"' + \
            '            accept="' + imageFormats + '">\n' + \
            '      <br><label class="labels">' + \
            translate['Right column image'] + '</label>\n' + \
            '      <input type="file" id="right_col_image" ' + \
            'name="right_col_image"' + \
            '            accept="' + imageFormats + '">\n'

    editProfileForm += endEditSection()
    return editProfileForm


def _htmlEditProfileContactInfo(nickname: str,
                                emailAddress: str,
                                xmppAddress: str,
                                matrixAddress: str,
                                ssbAddress: str,
                                toxAddress: str,
                                briarAddress: str,
                                jamiAddress: str,
                                cwtchAddress: str,
                                PGPfingerprint: str,
                                PGPpubKey: str,
                                translate: {}) -> str:
    """Contact Information section of edit profile screen
    """
    editProfileForm = beginEditSection(translate['Contact Details'])

    editProfileForm += editTextField(translate['Email'],
                                     'email', emailAddress)
    editProfileForm += editTextField(translate['XMPP'],
                                     'xmppAddress', xmppAddress)
    editProfileForm += editTextField(translate['Matrix'],
                                     'matrixAddress', matrixAddress)
    editProfileForm += editTextField('SSB', 'ssbAddress', ssbAddress)
    editProfileForm += editTextField('Tox', 'toxAddress', toxAddress)
    editProfileForm += editTextField('Briar', 'briarAddress', briarAddress)
    editProfileForm += editTextField('Jami', 'jamiAddress', jamiAddress)
    editProfileForm += editTextField('Cwtch', 'cwtchAddress', cwtchAddress)
    editProfileForm += editTextField(translate['PGP Fingerprint'],
                                     'openpgp', PGPfingerprint)
    editProfileForm += \
        editTextArea(translate['PGP'], 'pgp', PGPpubKey, 600,
                     '-----BEGIN PGP PUBLIC KEY BLOCK-----', False)
    editProfileForm += \
        '<a href="/users/' + nickname + \
        '/followingaccounts"><label class="labels">' + \
        translate['Following'] + '</label></a><br>\n'

    editProfileForm += endEditSection()
    return editProfileForm


def _htmlEditProfileOptions(manuallyApprovesFollowers: str,
                            isBot: str, isGroup: str,
                            followDMs: str, removeTwitter: str,
                            notifyLikes: str, hideLikeButton: str,
                            translate: {}) -> str:
    """option checkboxes section of edit profile screen
    """
    editProfileForm = '    <div class="container">\n'
    editProfileForm += \
        editCheckBox(translate['Approve follower requests'],
                     'approveFollowers', manuallyApprovesFollowers)
    editProfileForm += \
        editCheckBox(translate['This is a bot account'],
                     'isBot', isBot)
    editProfileForm += \
        editCheckBox(translate['This is a group account'],
                     'isGroup', isGroup)
    editProfileForm += \
        editCheckBox(translate['Only people I follow can send me DMs'],
                     'followDMs', followDMs)
    editProfileForm += \
        editCheckBox(translate['Remove Twitter posts'],
                     'removeTwitter', removeTwitter)
    editProfileForm += \
        editCheckBox(translate['Notify when posts are liked'],
                     'notifyLikes', notifyLikes)
    editProfileForm += \
        editCheckBox(translate["Don't show the Like button"],
                     'hideLikeButton', hideLikeButton)
    editProfileForm += '    </div>\n'
    return editProfileForm


def _getSupportedLanguages(baseDir: str) -> str:
    """Returns a list of supported languages
    """
    langList = []
    for subdir, dirs, files in os.walk(baseDir + '/translations'):
        for f in files:
            if not f.endswith('.json'):
                continue
            langStr = f.split('.')[0]
            if len(langStr) != 2:
                continue
            langList.append(langStr)
        break
    if not langList:
        return ''
    langList.sort()
    languagesStr = ''
    for lang in langList:
        if languagesStr:
            languagesStr += ' / ' + lang
        else:
            languagesStr = lang
    return languagesStr


def _htmlEditProfileMain(baseDir: str, displayNickname: str, bioStr: str,
                         movedTo: str, donateUrl: str,
                         blogAddress: str, actorJson: {},
                         translate: {}) -> str:
    """main info on edit profile screen
    """
    imageFormats = getImageFormats()

    editProfileForm = '    <div class="container">\n'

    editProfileForm += \
        editTextField(translate['Nickname'], 'displayNickname',
                      displayNickname)

    editProfileForm += \
        editTextArea(translate['Your bio'], 'bio', bioStr, 200, '', True)

    editProfileForm += \
        '      <label class="labels">' + translate['Avatar image'] + \
        '</label>\n' + \
        '      <input type="file" id="avatar" name="avatar"' + \
        '            accept="' + imageFormats + '">\n'

    occupationName = ''
    if actorJson.get('hasOccupation'):
        occupationName = getOccupationName(actorJson)

    editProfileForm += \
        editTextField(translate['Occupation'], 'occupationName',
                      occupationName)

    alsoKnownAsStr = ''
    if actorJson.get('alsoKnownAs'):
        alsoKnownAs = actorJson['alsoKnownAs']
        ctr = 0
        for altActor in alsoKnownAs:
            if ctr > 0:
                alsoKnownAsStr += ', '
            ctr += 1
            alsoKnownAsStr += altActor

    editProfileForm += \
        editTextField(translate['Other accounts'], 'alsoKnownAs',
                      alsoKnownAsStr, 'https://...')

    editProfileForm += \
        editTextField(translate['Moved to new account address'], 'movedTo',
                      movedTo, 'https://...')

    editProfileForm += \
        editTextField(translate['Donations link'], 'donateUrl',
                      donateUrl, 'https://...')

    editProfileForm += \
        editTextField('Blog', 'blogAddress', blogAddress, 'https://...')

    languagesListStr = _getSupportedLanguages(baseDir)
    showLanguages = getActorLanguages(actorJson)
    editProfileForm += \
        editTextField(translate['Languages'], 'showLanguages',
                      showLanguages, languagesListStr)

    editProfileForm += '    </div>\n'
    return editProfileForm


def _htmlEditProfileTopBanner(baseDir: str,
                              nickname: str, domain: str, domainFull: str,
                              defaultTimeline: str, bannerFile: str,
                              path: str, accessKeys: {}, translate: {}) -> str:
    """top banner on edit profile screen
    """
    editProfileForm = \
        '<a href="/users/' + nickname + '/' + defaultTimeline + '">' + \
        '<img loading="lazy" class="timeline-banner" src="' + \
        '/users/' + nickname + '/' + bannerFile + '" alt="" /></a>\n'

    editProfileForm += \
        '<form enctype="multipart/form-data" method="POST" ' + \
        'accept-charset="UTF-8" action="' + path + '/profiledata">\n'
    editProfileForm += '  <div class="vertical-center">\n'
    editProfileForm += \
        '    <h1>' + translate['Profile for'] + \
        ' ' + nickname + '@' + domainFull + '</h1>'
    editProfileForm += '    <div class="container">\n'
    editProfileForm += \
        '      <center>\n' + \
        '        <input type="submit" name="submitProfile" ' + \
        'accesskey="' + accessKeys['submitButton'] + '" ' + \
        'value="' + translate['Submit'] + '">\n' + \
        '      </center>\n'
    editProfileForm += '    </div>\n'

    if scheduledPostsExist(baseDir, nickname, domain):
        editProfileForm += '    <div class="container">\n'
        editProfileForm += \
            editCheckBox(translate['Remove scheduled posts'],
                         'removeScheduledPosts', False)
        editProfileForm += '    </div>\n'
    return editProfileForm


def htmlEditProfile(cssCache: {}, translate: {}, baseDir: str, path: str,
                    domain: str, port: int, httpPrefix: str,
                    defaultTimeline: str, theme: str,
                    peertubeInstances: [],
                    textModeBanner: str, city: str,
                    userAgentsBlocked: str,
                    accessKeys: {}) -> str:
    """Shows the edit profile screen
    """
    path = path.replace('/inbox', '').replace('/outbox', '')
    path = path.replace('/shares', '')
    nickname = getNicknameFromActor(path)
    if not nickname:
        return ''
    domainFull = getFullDomain(domain, port)

    actorFilename = acctDir(baseDir, nickname, domain) + '.json'
    if not os.path.isfile(actorFilename):
        return ''

    # filename of the banner shown at the top
    bannerFile, bannerFilename = \
        getBannerFile(baseDir, nickname, domain, theme)

    displayNickname = nickname
    isBot = isGroup = followDMs = removeTwitter = ''
    notifyLikes = hideLikeButton = mediaInstanceStr = ''
    blogsInstanceStr = newsInstanceStr = movedTo = ''
    bioStr = donateUrl = emailAddress = PGPpubKey = ''
    PGPfingerprint = xmppAddress = matrixAddress = ''
    ssbAddress = blogAddress = toxAddress = jamiAddress = ''
    cwtchAddress = briarAddress = manuallyApprovesFollowers = ''

    actorJson = loadJson(actorFilename)
    if actorJson:
        if actorJson.get('movedTo'):
            movedTo = actorJson['movedTo']
        donateUrl = getDonationUrl(actorJson)
        xmppAddress = getXmppAddress(actorJson)
        matrixAddress = getMatrixAddress(actorJson)
        ssbAddress = getSSBAddress(actorJson)
        blogAddress = getBlogAddress(actorJson)
        toxAddress = getToxAddress(actorJson)
        briarAddress = getBriarAddress(actorJson)
        jamiAddress = getJamiAddress(actorJson)
        cwtchAddress = getCwtchAddress(actorJson)
        emailAddress = getEmailAddress(actorJson)
        PGPpubKey = getPGPpubKey(actorJson)
        PGPfingerprint = getPGPfingerprint(actorJson)
        if actorJson.get('name'):
            if not isFiltered(baseDir, nickname, domain, actorJson['name']):
                displayNickname = actorJson['name']
        if actorJson.get('summary'):
            bioStr = \
                actorJson['summary'].replace('<p>', '').replace('</p>', '')
            if isFiltered(baseDir, nickname, domain, bioStr):
                bioStr = ''
        if actorJson.get('manuallyApprovesFollowers'):
            if actorJson['manuallyApprovesFollowers']:
                manuallyApprovesFollowers = 'checked'
            else:
                manuallyApprovesFollowers = ''
        if actorJson.get('type'):
            if actorJson['type'] == 'Service':
                isBot = 'checked'
                isGroup = ''
            elif actorJson['type'] == 'Group':
                isGroup = 'checked'
                isBot = ''
    accountDir = acctDir(baseDir, nickname, domain)
    if os.path.isfile(accountDir + '/.followDMs'):
        followDMs = 'checked'
    if os.path.isfile(accountDir + '/.removeTwitter'):
        removeTwitter = 'checked'
    if os.path.isfile(accountDir + '/.notifyLikes'):
        notifyLikes = 'checked'
    if os.path.isfile(accountDir + '/.hideLikeButton'):
        hideLikeButton = 'checked'

    mediaInstance = getConfigParam(baseDir, "mediaInstance")
    if mediaInstance:
        if mediaInstance is True:
            mediaInstanceStr = 'checked'
            blogsInstanceStr = newsInstanceStr = ''

    newsInstance = getConfigParam(baseDir, "newsInstance")
    if newsInstance:
        if newsInstance is True:
            newsInstanceStr = 'checked'
            blogsInstanceStr = mediaInstanceStr = ''

    blogsInstance = getConfigParam(baseDir, "blogsInstance")
    if blogsInstance:
        if blogsInstance is True:
            blogsInstanceStr = 'checked'
            mediaInstanceStr = newsInstanceStr = ''

    cssFilename = baseDir + '/epicyon-profile.css'
    if os.path.isfile(baseDir + '/epicyon.css'):
        cssFilename = baseDir + '/epicyon.css'

    instanceStr = ''
    roleAssignStr = ''
    peertubeStr = ''
    libretranslateStr = ''
    graphicsStr = ''

    adminNickname = getConfigParam(baseDir, 'admin')

    if isArtist(baseDir, nickname) or \
       path.startswith('/users/' + str(adminNickname) + '/'):
        graphicsStr = _htmlEditProfileGraphicDesign(baseDir, translate)

    if adminNickname:
        if path.startswith('/users/' + adminNickname + '/'):
            instanceStr, roleAssignStr, peertubeStr, libretranslateStr = \
                _htmlEditProfileInstance(baseDir, translate,
                                         peertubeInstances,
                                         mediaInstanceStr,
                                         blogsInstanceStr,
                                         newsInstanceStr)

    instanceTitle = getConfigParam(baseDir, 'instanceTitle')
    editProfileForm = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)

    # keyboard navigation
    userPathStr = '/users/' + nickname
    userTimalineStr = '/users/' + nickname + '/' + defaultTimeline
    menuTimeline = \
        htmlHideFromScreenReader('🏠') + ' ' + \
        translate['Switch to timeline view']
    menuProfile = \
        htmlHideFromScreenReader('👤') + ' ' + \
        translate['Switch to profile view']
    navLinks = {
        menuProfile: userPathStr,
        menuTimeline: userTimalineStr
    }
    navAccessKeys = {
        menuProfile: 'p',
        menuTimeline: 't'
    }
    editProfileForm += htmlKeyboardNavigation(textModeBanner,
                                              navLinks, navAccessKeys)

    # top banner
    editProfileForm += \
        _htmlEditProfileTopBanner(baseDir, nickname, domain, domainFull,
                                  defaultTimeline, bannerFile,
                                  path, accessKeys, translate)

    # main info
    editProfileForm += \
        _htmlEditProfileMain(baseDir, displayNickname, bioStr,
                             movedTo, donateUrl,
                             blogAddress, actorJson, translate)

    # Option checkboxes
    editProfileForm += \
        _htmlEditProfileOptions(manuallyApprovesFollowers,
                                isBot, isGroup, followDMs, removeTwitter,
                                notifyLikes, hideLikeButton, translate)

    # Contact information
    editProfileForm += \
        _htmlEditProfileContactInfo(nickname, emailAddress,
                                    xmppAddress, matrixAddress,
                                    ssbAddress, toxAddress,
                                    briarAddress, jamiAddress,
                                    cwtchAddress, PGPfingerprint,
                                    PGPpubKey, translate)

    # Customize images and banners
    editProfileForm += _htmlEditProfileBackground(newsInstance, translate)

    # Change password
    editProfileForm += _htmlEditProfileChangePassword(translate)

    # automatic translations
    editProfileForm += libretranslateStr

    # Filtering and blocking section
    editProfileForm += \
        _htmlEditProfileFiltering(baseDir, nickname, domain,
                                  userAgentsBlocked, translate)

    # git projects section
    editProfileForm += \
        _htmlEditProfileGitProjects(baseDir, nickname, domain, translate)

    # Skills section
    editProfileForm += \
        _htmlEditProfileSkills(baseDir, nickname, domain, translate)

    editProfileForm += roleAssignStr + peertubeStr + graphicsStr + instanceStr

    # danger zone section
    editProfileForm += _htmlEditProfileDangerZone(translate)

    editProfileForm += '    <div class="container">\n'
    editProfileForm += \
        '      <center>\n' + \
        '        <input type="submit" name="submitProfile" value="' + \
        translate['Submit'] + '">\n' + \
        '      </center>\n'
    editProfileForm += '    </div>\n'

    editProfileForm += '  </div>\n'
    editProfileForm += '</form>\n'
    editProfileForm += htmlFooter()
    return editProfileForm


def _individualFollowAsHtml(translate: {},
                            baseDir: str, session,
                            cachedWebfingers: {},
                            personCache: {}, domain: str,
                            followUrl: str,
                            authorized: bool,
                            actorNickname: str,
                            httpPrefix: str,
                            projectVersion: str,
                            dormant: bool,
                            debug: bool,
                            buttons=[]) -> str:
    """An individual follow entry on the profile screen
    """
    followUrlNickname = getNicknameFromActor(followUrl)
    followUrlDomain, followUrlPort = getDomainFromActor(followUrl)
    followUrlDomainFull = getFullDomain(followUrlDomain, followUrlPort)
    titleStr = '@' + followUrlNickname + '@' + followUrlDomainFull
    avatarUrl = getPersonAvatarUrl(baseDir, followUrl, personCache, True)
    if not avatarUrl:
        avatarUrl = followUrl + '/avatar.png'

    # lookup the correct webfinger for the followUrl
    followUrlHandle = followUrlNickname + '@' + followUrlDomainFull
    followUrlWf = \
        webfingerHandle(session, followUrlHandle, httpPrefix,
                        cachedWebfingers,
                        domain, __version__, debug)

    (inboxUrl, pubKeyId, pubKey,
     fromPersonId, sharedInbox,
     avatarUrl2, displayName) = getPersonBox(baseDir, session,
                                             followUrlWf,
                                             personCache, projectVersion,
                                             httpPrefix, followUrlNickname,
                                             domain, 'outbox', 43036)
    if avatarUrl2:
        avatarUrl = avatarUrl2
    if displayName:
        displayName = \
            addEmojiToDisplayName(baseDir, httpPrefix,
                                  actorNickname, domain,
                                  displayName, False)
        titleStr = displayName

    if dormant:
        titleStr += ' 💤'

    buttonsStr = ''
    if authorized:
        for b in buttons:
            if b == 'block':
                buttonsStr += \
                    '<a href="/users/' + actorNickname + \
                    '?options=' + followUrl + \
                    ';1;' + avatarUrl + '"><button class="buttonunfollow">' + \
                    translate['Block'] + '</button></a>\n'
            elif b == 'unfollow':
                buttonsStr += \
                    '<a href="/users/' + actorNickname + \
                    '?options=' + followUrl + \
                    ';1;' + avatarUrl + '"><button class="buttonunfollow">' + \
                    translate['Unfollow'] + '</button></a>\n'

    resultStr = '<div class="container">\n'
    resultStr += \
        '<a href="/users/' + actorNickname + '?options=' + \
        followUrl + ';1;' + avatarUrl + '">\n'
    resultStr += '<p><img loading="lazy" src="' + avatarUrl + '" alt=" ">'
    resultStr += titleStr + '</a>' + buttonsStr + '</p>\n'
    resultStr += '</div>\n'
    return resultStr