Repository: streetcomplete/StreetComplete (language: kotlin)

Issue:
Weekly Scores and Ranks
I've played SC with a friend and she was quite disappointed that she can't "beat" me, cause I had a headstart of month. 

I was wondering if the game and competition factor wouldn't work better if we had weekly stats instead of epoch ones. 

We currently have a lot of empty real estate on the profile page:

![Screenshot_20210604-153309769](https://user-images.githubusercontent.com/614929/120810065-05798080-c54b-11eb-82d9-4a8521c86a69.jpg)

Maybe we could move the current stats down to the bottom and have a weekly counter in the top for the current week and show the results of the last week below. 

This way friends can compete which eachother on a weekly basis. 


Discussion hints:
If I'm correct, this might also require modifications to the [web server statistics](https://github.com/streetcomplete/sc-statistics-service), as well as the main app.  Anyway, it seems a reasonable idea.  I wonder what other statistics one might include.
We could in theory query the OSM profile and get the OSM friends and their stats couldn't be shown there. 🤔
Technically, this is well possible and actually not that much effort. I would accept a PR that implements this. Actually, it would need to be two PRs, one for the UI here, one for the generation of the stats, as @10992-osm correctly mentions.
As a first version I would try to do something like the following. While there is definitely room for improving the design, I think this should be sufficient as a first step.

![profile](https://user-images.githubusercontent.com/32853499/138177423-c9e81183-303c-4a48-9985-63b5241d01ec.png)


Hmm, why show last week? You can't change anything from last week, I think it might be better to only show things that the user can improve (by contributing). If it is to show a (positive?) change, there is also another issue/idea to animate the changes since one last had a look in the profile screen.
Maybe you can replace "last week" with "current month"
> Hmm, why show last week?

I thought about it as "the results of last weeks competition". You might be right though, I like "current month" as well.

Pull request: enable display of weekly stats
fixes #2946

Test command: ./gradlew :app:testDebugUnitTest --tests "de.westnordost.streetcomplete.data.user.statistics.StatisticsParserTest" --tests "de.westnordost.streetcomplete.data.user.statistics.StatisticsControllerTest"

Fix patch:
diff --git a/app/src/main/java/de/westnordost/streetcomplete/Prefs.kt b/app/src/main/java/de/westnordost/streetcomplete/Prefs.kt
index f3769f6a9e3..5c2d67930ac 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/Prefs.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/Prefs.kt
@@ -24,6 +24,7 @@ object Prefs {
     const val OSM_LOGGED_IN_AFTER_OAUTH_FUCKUP = "osm.logged_in_after_oauth_fuckup"
     const val USER_DAYS_ACTIVE = "days_active"
     const val USER_GLOBAL_RANK = "user_global_rank"
+    const val USER_GLOBAL_RANK_CURRENT_WEEK = "user_global_rank_current_week"
     const val USER_LAST_TIMESTAMP_ACTIVE = "last_timestamp_active"
     const val IS_SYNCHRONIZING_STATISTICS = "is_synchronizing_statistics"
     const val TEAM_MODE_INDEX_IN_TEAM = "team_mode.index_in_team"
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/StreetCompleteSQLiteOpenHelper.kt b/app/src/main/java/de/westnordost/streetcomplete/data/StreetCompleteSQLiteOpenHelper.kt
index 7e74bdbb485..ba01cb63072 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/StreetCompleteSQLiteOpenHelper.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/StreetCompleteSQLiteOpenHelper.kt
@@ -20,8 +20,8 @@ import de.westnordost.streetcomplete.data.osmnotes.edits.NoteEditsTable
 import de.westnordost.streetcomplete.data.osmnotes.notequests.NoteQuestsHiddenTable
 import de.westnordost.streetcomplete.data.user.achievements.UserAchievementsTable
 import de.westnordost.streetcomplete.data.user.achievements.UserLinksTable
-import de.westnordost.streetcomplete.data.user.statistics.CountryStatisticsTable
-import de.westnordost.streetcomplete.data.user.statistics.EditTypeStatisticsTable
+import de.westnordost.streetcomplete.data.user.statistics.CountryStatisticsTables
+import de.westnordost.streetcomplete.data.user.statistics.EditTypeStatisticsTables
 import de.westnordost.streetcomplete.data.visiblequests.QuestPresetsTable
 import de.westnordost.streetcomplete.data.visiblequests.QuestTypeOrderTable
 import de.westnordost.streetcomplete.data.visiblequests.VisibleQuestTypeTable
@@ -84,8 +84,10 @@ class StreetCompleteSQLiteOpenHelper(context: Context, dbName: String) :
         db.execSQL(DownloadedTilesTable.CREATE)
 
         // user statistics
-        db.execSQL(EditTypeStatisticsTable.CREATE)
-        db.execSQL(CountryStatisticsTable.CREATE)
+        db.execSQL(EditTypeStatisticsTables.create(EditTypeStatisticsTables.NAME))
+        db.execSQL(EditTypeStatisticsTables.create(EditTypeStatisticsTables.NAME_CURRENT_WEEK))
+        db.execSQL(CountryStatisticsTables.create(CountryStatisticsTables.NAME))
+        db.execSQL(CountryStatisticsTables.create(CountryStatisticsTables.NAME_CURRENT_WEEK))
         db.execSQL(UserAchievementsTable.CREATE)
         db.execSQL(UserLinksTable.CREATE)
 
@@ -174,7 +176,11 @@ class StreetCompleteSQLiteOpenHelper(context: Context, dbName: String) :
         if (oldVersion <= 5 && newVersion > 5) {
             db.execSQL("ALTER TABLE ${NoteEditsTable.NAME} ADD COLUMN ${NoteEditsTable.Columns.TRACK} text DEFAULT '[]' NOT NULL")
         }
+        if (oldVersion <= 6 && newVersion > 6) {
+            db.execSQL(EditTypeStatisticsTables.create(EditTypeStatisticsTables.NAME_CURRENT_WEEK))
+            db.execSQL(CountryStatisticsTables.create(CountryStatisticsTables.NAME_CURRENT_WEEK))
+        }
     }
 }
 
-private const val DB_VERSION = 6
+private const val DB_VERSION = 7
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/CountryStatisticsDao.kt b/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/CountryStatisticsDao.kt
index 0e3b0cc9f7b..6f73a0f9368 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/CountryStatisticsDao.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/CountryStatisticsDao.kt
@@ -2,29 +2,28 @@ package de.westnordost.streetcomplete.data.user.statistics
 
 import de.westnordost.streetcomplete.data.CursorPosition
 import de.westnordost.streetcomplete.data.Database
-import de.westnordost.streetcomplete.data.user.statistics.CountryStatisticsTable.Columns.COUNTRY_CODE
-import de.westnordost.streetcomplete.data.user.statistics.CountryStatisticsTable.Columns.RANK
-import de.westnordost.streetcomplete.data.user.statistics.CountryStatisticsTable.Columns.SUCCEEDED
-import de.westnordost.streetcomplete.data.user.statistics.CountryStatisticsTable.NAME
+import de.westnordost.streetcomplete.data.user.statistics.CountryStatisticsTables.Columns.COUNTRY_CODE
+import de.westnordost.streetcomplete.data.user.statistics.CountryStatisticsTables.Columns.RANK
+import de.westnordost.streetcomplete.data.user.statistics.CountryStatisticsTables.Columns.SUCCEEDED
 
 /** Stores how many quests the user solved in which country */
-class CountryStatisticsDao(private val db: Database) {
+class CountryStatisticsDao(private val db: Database, private val name: String) {
 
     fun getCountryWithBiggestSolvedCount(): CountryStatistics? =
-        db.queryOne(NAME, orderBy = "$SUCCEEDED DESC") { it.toCountryStatistics() }
+        db.queryOne(name, orderBy = "$SUCCEEDED DESC") { it.toCountryStatistics() }
 
     fun getAll(): List<CountryStatistics> =
-        db.query(NAME) { it.toCountryStatistics() }
+        db.query(name) { it.toCountryStatistics() }
 
     fun clear() {
-        db.delete(NAME)
+        db.delete(name)
     }
 
     fun replaceAll(countriesStatistics: Collection<CountryStatistics>) {
         db.transaction {
-            db.delete(NAME)
+            db.delete(name)
             if (countriesStatistics.isNotEmpty()) {
-                db.replaceMany(NAME,
+                db.replaceMany(name,
                     arrayOf(COUNTRY_CODE, SUCCEEDED, RANK),
                     countriesStatistics.map { arrayOf(it.countryCode, it.count, it.rank) }
                 )
@@ -35,18 +34,18 @@ class CountryStatisticsDao(private val db: Database) {
     fun addOne(countryCode: String) {
         db.transaction {
             // first ensure the row exists
-            db.insertOrIgnore(NAME, listOf(
+            db.insertOrIgnore(name, listOf(
                 COUNTRY_CODE to countryCode,
                 SUCCEEDED to 0
             ))
 
             // then increase by one
-            db.exec("UPDATE $NAME SET $SUCCEEDED = $SUCCEEDED + 1 WHERE $COUNTRY_CODE = ?", arrayOf(countryCode))
+            db.exec("UPDATE $name SET $SUCCEEDED = $SUCCEEDED + 1 WHERE $COUNTRY_CODE = ?", arrayOf(countryCode))
         }
     }
 
     fun subtractOne(countryCode: String) {
-        db.exec("UPDATE $NAME SET $SUCCEEDED = $SUCCEEDED - 1 WHERE $COUNTRY_CODE = ?", arrayOf(countryCode))
+        db.exec("UPDATE $name SET $SUCCEEDED = $SUCCEEDED - 1 WHERE $COUNTRY_CODE = ?", arrayOf(countryCode))
     }
 }
 
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/CountryStatisticsTable.kt b/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/CountryStatisticsTables.kt
similarity index 71%
rename from app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/CountryStatisticsTable.kt
rename to app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/CountryStatisticsTables.kt
index 2eab3657a46..b86faf31dda 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/CountryStatisticsTable.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/CountryStatisticsTables.kt
@@ -1,7 +1,8 @@
 package de.westnordost.streetcomplete.data.user.statistics
 
-object CountryStatisticsTable {
+object CountryStatisticsTables {
     const val NAME = "country_statistics"
+    const val NAME_CURRENT_WEEK = "country_statistics_current_week"
 
     object Columns {
         const val COUNTRY_CODE = "country_code"
@@ -9,8 +10,8 @@ object CountryStatisticsTable {
         const val RANK = "rank"
     }
 
-    const val CREATE = """
-        CREATE TABLE $NAME (
+    fun create(name: String) = """
+        CREATE TABLE $name (
             ${Columns.COUNTRY_CODE} varchar(255) PRIMARY KEY,
             ${Columns.SUCCEEDED} int NOT NULL,
             ${Columns.RANK} int
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/EditTypeStatisticsDao.kt b/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/EditTypeStatisticsDao.kt
index cb09a86ce6d..2ac7f877590 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/EditTypeStatisticsDao.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/EditTypeStatisticsDao.kt
@@ -2,28 +2,27 @@ package de.westnordost.streetcomplete.data.user.statistics
 
 import de.westnordost.streetcomplete.data.CursorPosition
 import de.westnordost.streetcomplete.data.Database
-import de.westnordost.streetcomplete.data.user.statistics.EditTypeStatisticsTable.Columns.ELEMENT_EDIT_TYPE
-import de.westnordost.streetcomplete.data.user.statistics.EditTypeStatisticsTable.Columns.SUCCEEDED
-import de.westnordost.streetcomplete.data.user.statistics.EditTypeStatisticsTable.NAME
+import de.westnordost.streetcomplete.data.user.statistics.EditTypeStatisticsTables.Columns.ELEMENT_EDIT_TYPE
+import de.westnordost.streetcomplete.data.user.statistics.EditTypeStatisticsTables.Columns.SUCCEEDED
 
 /** Stores how many edits of which element type the user did */
-class EditTypeStatisticsDao(private val db: Database) {
+class EditTypeStatisticsDao(private val db: Database, private val name: String) {
 
     fun getTotalAmount(): Int =
-        db.queryOne(NAME, arrayOf("total($SUCCEEDED) as count")) { it.getInt("count") } ?: 0
+        db.queryOne(name, arrayOf("total($SUCCEEDED) as count")) { it.getInt("count") } ?: 0
 
     fun getAll(): List<EditTypeStatistics> =
-        db.query(NAME) { it.toEditTypeStatistics() }
+        db.query(name) { it.toEditTypeStatistics() }
 
     fun clear() {
-        db.delete(NAME)
+        db.delete(name)
     }
 
     fun replaceAll(amounts: Map<String, Int>) {
         db.transaction {
-            db.delete(NAME)
+            db.delete(name)
             if (amounts.isNotEmpty()) {
-                db.replaceMany(NAME,
+                db.replaceMany(name,
                     arrayOf(ELEMENT_EDIT_TYPE, SUCCEEDED),
                     amounts.map { arrayOf(it.key, it.value) }
                 )
@@ -34,22 +33,22 @@ class EditTypeStatisticsDao(private val db: Database) {
     fun addOne(type: String) {
         db.transaction {
             // first ensure the row exists
-            db.insertOrIgnore(NAME, listOf(
+            db.insertOrIgnore(name, listOf(
                 ELEMENT_EDIT_TYPE to type,
                 SUCCEEDED to 0
             ))
 
             // then increase by one
-            db.exec("UPDATE $NAME SET $SUCCEEDED = $SUCCEEDED + 1 WHERE $ELEMENT_EDIT_TYPE = ?", arrayOf(type))
+            db.exec("UPDATE $name SET $SUCCEEDED = $SUCCEEDED + 1 WHERE $ELEMENT_EDIT_TYPE = ?", arrayOf(type))
         }
     }
 
     fun subtractOne(type: String) {
-        db.exec("UPDATE $NAME SET $SUCCEEDED = $SUCCEEDED - 1 WHERE $ELEMENT_EDIT_TYPE = ?", arrayOf(type))
+        db.exec("UPDATE $name SET $SUCCEEDED = $SUCCEEDED - 1 WHERE $ELEMENT_EDIT_TYPE = ?", arrayOf(type))
     }
 
     fun getAmount(type: String): Int =
-        db.queryOne(NAME,
+        db.queryOne(name,
             columns = arrayOf(SUCCEEDED),
             where = "$ELEMENT_EDIT_TYPE = ?",
             args = arrayOf(type)
@@ -57,7 +56,7 @@ class EditTypeStatisticsDao(private val db: Database) {
 
     fun getAmount(type: List<String>): Int {
         val questionMarks = Array(type.size) { "?" }.joinToString(",")
-        return db.queryOne(NAME,
+        return db.queryOne(name,
             columns = arrayOf("total($SUCCEEDED) as count"),
             where = "$ELEMENT_EDIT_TYPE in ($questionMarks)",
             args = type.toTypedArray()
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/EditTypeStatisticsTable.kt b/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/EditTypeStatisticsTables.kt
similarity index 68%
rename from app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/EditTypeStatisticsTable.kt
rename to app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/EditTypeStatisticsTables.kt
index aa89e3b3fae..dea725751ff 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/EditTypeStatisticsTable.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/EditTypeStatisticsTables.kt
@@ -1,15 +1,16 @@
 package de.westnordost.streetcomplete.data.user.statistics
 
-object EditTypeStatisticsTable {
+object EditTypeStatisticsTables {
     const val NAME = "quest_statistics"
+    const val NAME_CURRENT_WEEK = "quest_statistics_current_week"
 
     object Columns {
         const val ELEMENT_EDIT_TYPE = "quest_type"
         const val SUCCEEDED = "succeeded"
     }
 
-    const val CREATE = """
-        CREATE TABLE $NAME (
+    fun create(name: String) = """
+        CREATE TABLE $name (
             ${Columns.ELEMENT_EDIT_TYPE} varchar(255) PRIMARY KEY,
             ${Columns.SUCCEEDED} int NOT NULL
         );
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/Statistics.kt b/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/Statistics.kt
index b6766d3d7c3..f06a7e10311 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/Statistics.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/Statistics.kt
@@ -5,8 +5,11 @@ data class Statistics(
     val countries: List<CountryStatistics>,
     val rank: Int,
     val daysActive: Int,
+    val currentWeekRank: Int,
+    val currentWeekTypes: List<EditTypeStatistics>,
+    val currentWeekCountries: List<CountryStatistics>,
     val lastUpdate: Long,
-    val isAnalyzing: Boolean
+    val isAnalyzing: Boolean,
 )
 
 data class CountryStatistics(val countryCode: String, val count: Int, val rank: Int?)
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/StatisticsController.kt b/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/StatisticsController.kt
index 384e36c8048..90ed28561f3 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/StatisticsController.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/StatisticsController.kt
@@ -19,6 +19,8 @@ import java.util.concurrent.FutureTask
 class StatisticsController(
     private val editTypeStatisticsDao: EditTypeStatisticsDao,
     private val countryStatisticsDao: CountryStatisticsDao,
+    private val currentWeekEditTypeStatisticsDao: EditTypeStatisticsDao,
+    private val currentWeekCountryStatisticsDao: CountryStatisticsDao,
     private val countryBoundaries: FutureTask<CountryBoundaries>,
     private val prefs: SharedPreferences,
     userLoginStatusSource: UserLoginStatusSource
@@ -45,6 +47,12 @@ class StatisticsController(
             prefs.edit(true) { putInt(Prefs.USER_DAYS_ACTIVE, value) }
         }
 
+    override var currentWeekRank: Int
+        get() = prefs.getInt(Prefs.USER_GLOBAL_RANK_CURRENT_WEEK, -1)
+        private set(value) {
+            prefs.edit(true) { putInt(Prefs.USER_GLOBAL_RANK_CURRENT_WEEK, value) }
+        }
+
     override var isSynchronizing: Boolean
         // default true because if it is not set yet, the first thing that is done is to synchronize it
         get() = prefs.getBoolean(Prefs.IS_SYNCHRONIZING_STATISTICS, true)
@@ -80,16 +88,36 @@ class StatisticsController(
     override fun getCountryStatisticsOfCountryWithBiggestSolvedCount() =
         countryStatisticsDao.getCountryWithBiggestSolvedCount()
 
+    override fun getCurrentWeekEditCount(): Int =
+        currentWeekEditTypeStatisticsDao.getTotalAmount()
+
+    override fun getCurrentWeekEditTypeStatistics(): List<EditTypeStatistics> =
+        currentWeekEditTypeStatisticsDao.getAll()
+
+    override fun getCurrentWeekCountryStatistics(): List<CountryStatistics> =
+        currentWeekCountryStatisticsDao.getAll()
+
+    override fun getCurrentWeekCountryStatisticsOfCountryWithBiggestSolvedCount(): CountryStatistics? =
+        currentWeekCountryStatisticsDao.getCountryWithBiggestSolvedCount()
+
     fun addOne(type: String, position: LatLon) {
         editTypeStatisticsDao.addOne(type)
-        getRealCountryCode(position)?.let { countryStatisticsDao.addOne(it) }
+        currentWeekEditTypeStatisticsDao.addOne(type)
+        getRealCountryCode(position)?.let {
+            countryStatisticsDao.addOne(it)
+            currentWeekCountryStatisticsDao.addOne(it)
+        }
         listeners.forEach { it.onAddedOne(type) }
         updateDaysActive()
     }
 
     fun subtractOne(type: String, position: LatLon) {
         editTypeStatisticsDao.subtractOne(type)
-        getRealCountryCode(position)?.let { countryStatisticsDao.subtractOne(it) }
+        currentWeekEditTypeStatisticsDao.subtractOne(type)
+        getRealCountryCode(position)?.let {
+            countryStatisticsDao.subtractOne(it)
+            currentWeekCountryStatisticsDao.subtractOne(it)
+        }
         listeners.forEach { it.onSubtractedOne(type) }
         updateDaysActive()
     }
@@ -109,6 +137,9 @@ class StatisticsController(
 
         editTypeStatisticsDao.replaceAll(statistics.types.associate { it.type to it.count })
         countryStatisticsDao.replaceAll(statistics.countries)
+        currentWeekEditTypeStatisticsDao.replaceAll(statistics.currentWeekTypes.associate { it.type to it.count })
+        currentWeekCountryStatisticsDao.replaceAll(statistics.currentWeekCountries)
+        currentWeekRank = statistics.currentWeekRank
         rank = statistics.rank
         daysActive = statistics.daysActive
         lastUpdate = statistics.lastUpdate
@@ -119,10 +150,13 @@ class StatisticsController(
     private fun clear() {
         editTypeStatisticsDao.clear()
         countryStatisticsDao.clear()
+        currentWeekEditTypeStatisticsDao.clear()
+        currentWeekCountryStatisticsDao.clear()
         prefs.edit(true) {
             remove(Prefs.USER_DAYS_ACTIVE)
             remove(Prefs.IS_SYNCHRONIZING_STATISTICS)
             remove(Prefs.USER_GLOBAL_RANK)
+            remove(Prefs.USER_GLOBAL_RANK_CURRENT_WEEK)
             remove(Prefs.USER_LAST_TIMESTAMP_ACTIVE)
         }
 
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/StatisticsModule.kt b/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/StatisticsModule.kt
index a3d2fabc4c5..622dbbaa316 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/StatisticsModule.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/StatisticsModule.kt
@@ -5,11 +5,24 @@ import org.koin.dsl.module
 
 private const val STATISTICS_BACKEND_URL = "https://www.westnordost.de/streetcomplete/statistics/"
 val statisticsModule = module {
-    factory { CountryStatisticsDao(get()) }
-    factory { EditTypeStatisticsDao(get()) }
+
+    factory(named("EditTypeStatistics")) { EditTypeStatisticsDao(get(), EditTypeStatisticsTables.NAME) }
+    factory(named("CountryStatistics")) { CountryStatisticsDao(get(), CountryStatisticsTables.NAME) }
+
+    factory(named("EditTypeStatisticsCurrentWeek")) { EditTypeStatisticsDao(get(), EditTypeStatisticsTables.NAME_CURRENT_WEEK) }
+    factory(named("CountryStatisticsCurrentWeek")) { CountryStatisticsDao(get(), CountryStatisticsTables.NAME_CURRENT_WEEK) }
+
     factory { StatisticsDownloader(STATISTICS_BACKEND_URL, get()) }
     factory { StatisticsParser(get(named("TypeAliases"))) }
 
     single<StatisticsSource> { get<StatisticsController>() }
-    single { StatisticsController(get(), get(), get(named("CountryBoundariesFuture")), get(), get()) }
+    single { StatisticsController(
+        editTypeStatisticsDao = get(named("EditTypeStatistics")),
+        countryStatisticsDao = get(named("CountryStatistics")),
+        currentWeekEditTypeStatisticsDao = get(named("EditTypeStatisticsCurrentWeek")),
+        currentWeekCountryStatisticsDao = get(named("CountryStatisticsCurrentWeek")),
+        countryBoundaries = get(named("CountryBoundariesFuture")),
+        prefs = get(),
+        userLoginStatusSource = get()
+    ) }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/StatisticsParser.kt b/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/StatisticsParser.kt
index 406238cc70a..99a46d349f7 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/StatisticsParser.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/StatisticsParser.kt
@@ -6,29 +6,56 @@ import org.json.JSONObject
 class StatisticsParser(private val typeAliases: List<Pair<String, String>>) {
     fun parse(json: String): Statistics {
         val obj = JSONObject(json)
-        val questTypesJson = obj.getJSONObject("questTypes")
+
+        val typesStatistics = parseEditTypeStatistics(obj.getJSONObject("questTypes"))
+        val countriesStatistics = parseCountriesStatistics(
+            obj.getJSONObject("countries"),
+            obj.getJSONObject("countryRanks")
+        )
+        val rank = obj.getInt("rank")
+        val daysActive = obj.getInt("daysActive")
+        val isAnalyzing = obj.getBoolean("isAnalyzing")
+        val lastUpdate = Instant.parse(obj.getString("lastUpdate"))
+
+        val currentWeekRank = obj.getInt("currentWeekRank")
+        val currentWeekTypesStatistics = parseEditTypeStatistics(obj.getJSONObject("currentWeekQuestTypes"))
+        val currentWeekCountriesStatistics = parseCountriesStatistics(
+            obj.getJSONObject("currentWeekCountries"),
+            obj.getJSONObject("currentWeekCountryRanks")
+        )
+
+        return Statistics(
+            typesStatistics,
+            countriesStatistics,
+            rank,
+            daysActive,
+            currentWeekRank,
+            currentWeekTypesStatistics,
+            currentWeekCountriesStatistics,
+            lastUpdate.toEpochMilliseconds(),
+            isAnalyzing,
+        )
+    }
+
+    private fun parseEditTypeStatistics(obj: JSONObject): List<EditTypeStatistics> {
         val typesByName: MutableMap<String, Int> = mutableMapOf()
-        for (questTypeName in questTypesJson.keys()) {
-            typesByName[questTypeName] = questTypesJson.getInt(questTypeName)
+        for (questTypeName in obj.keys()) {
+            typesByName[questTypeName] = obj.getInt(questTypeName)
         }
         mergeTypeAliases(typesByName)
-        val typesStatistics = typesByName.map { EditTypeStatistics(it.key, it.value) }
-        val countriesJson = obj.getJSONObject("countries")
+        return typesByName.map { EditTypeStatistics(it.key, it.value) }
+    }
+
+    private fun parseCountriesStatistics(editsObj: JSONObject, ranksObj: JSONObject): List<CountryStatistics> {
         val countries: MutableMap<String, Int> = mutableMapOf()
-        for (country in countriesJson.keys()) {
-            countries[country] = countriesJson.getInt(country)
+        for (country in editsObj.keys()) {
+            countries[country] = editsObj.getInt(country)
         }
-        val countryRanksJson = obj.getJSONObject("countryRanks")
-        val countryRanks: MutableMap<String, Int> = mutableMapOf()
-        for (country in countryRanksJson.keys()) {
-            countryRanks[country] = countryRanksJson.getInt(country)
+        val ranks: MutableMap<String, Int> = mutableMapOf()
+        for (country in ranksObj.keys()) {
+            ranks[country] = ranksObj.getInt(country)
         }
-        val countriesStatistics = countries.map { CountryStatistics(it.key, it.value, countryRanks[it.key]) }
-        val rank = obj.getInt("rank")
-        val daysActive = obj.getInt("daysActive")
-        val isAnalyzing = obj.getBoolean("isAnalyzing")
-        val lastUpdate = Instant.parse(obj.getString("lastUpdate"))
-        return Statistics(typesStatistics, countriesStatistics, rank, daysActive, lastUpdate.toEpochMilliseconds(), isAnalyzing)
+        return countries.map { CountryStatistics(it.key, it.value, ranks[it.key]) }
     }
 
     private fun mergeTypeAliases(map: MutableMap<String, Int>) {
diff --git a/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/StatisticsSource.kt b/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/StatisticsSource.kt
index d8a50e589b0..ea892f85dc3 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/StatisticsSource.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/data/user/statistics/StatisticsSource.kt
@@ -23,7 +23,10 @@ interface StatisticsSource {
     /** Whether the statistics are still being synchronized with the backend */
     val isSynchronizing: Boolean
 
-    /** Return the total amount of quests solved*/
+    /** Users' global rank in the last 7 days. If <= 0, it's not set yet */
+    val currentWeekRank: Int
+
+    /** Return the total amount of quests solved */
     fun getEditCount(): Int
 
     /** Return amount of edits of the given type done */
@@ -38,9 +41,22 @@ interface StatisticsSource {
     /** Return all country statistics */
     fun getCountryStatistics(): List<CountryStatistics>
 
+    /** Return the total amount of quests solved in the last 7 days*/
+    fun getCurrentWeekEditCount(): Int
+
     /** Return the country statistics of the country in which the user solved the most quests, if any */
     fun getCountryStatisticsOfCountryWithBiggestSolvedCount(): CountryStatistics?
 
+    /** Return all edit type statistics of the last 7 days */
+    fun getCurrentWeekEditTypeStatistics(): List<EditTypeStatistics>
+
+    /** Return all country statistics of the last 7 days */
+    fun getCurrentWeekCountryStatistics(): List<CountryStatistics>
+
+    /** Return the country statistics of the country in which the user solved the most quests of the last 7 days, if any */
+    fun getCurrentWeekCountryStatisticsOfCountryWithBiggestSolvedCount(): CountryStatistics?
+
     fun addListener(listener: Listener)
     fun removeListener(listener: Listener)
+
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/screens/main/MainFragment.kt b/app/src/main/java/de/westnordost/streetcomplete/screens/main/MainFragment.kt
index 1c6902c33b1..5ad9304e598 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/screens/main/MainFragment.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/screens/main/MainFragment.kt
@@ -231,7 +231,6 @@ class MainFragment :
         binding.zoomInButton.setOnClickListener { onClickZoomIn() }
         binding.zoomOutButton.setOnClickListener { onClickZoomOut() }
         binding.createButton.setOnClickListener { onClickCreateButton() }
-        binding.answersCounterFragment.setOnClickListener { starInfoMenu() }
 
         updateOffsetWithOpenBottomSheet()
     }
@@ -741,10 +740,6 @@ class MainFragment :
         if (follow) mapFragment.centerCurrentPositionIfFollowing()
     }
 
-    fun starInfoMenu() {
-        val intent = Intent(requireContext(), UserActivity::class.java)
-        startActivity(intent)
-    }
     /* -------------------------------------- Context Menu -------------------------------------- */
 
     private fun showMapContextMenu(position: LatLon) {
diff --git a/app/src/main/java/de/westnordost/streetcomplete/screens/main/controls/AnswersCounterFragment.kt b/app/src/main/java/de/westnordost/streetcomplete/screens/main/controls/AnswersCounterFragment.kt
index 538025c2ef0..fd597104a38 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/screens/main/controls/AnswersCounterFragment.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/screens/main/controls/AnswersCounterFragment.kt
@@ -1,6 +1,8 @@
 package de.westnordost.streetcomplete.screens.main.controls
 
 import android.content.SharedPreferences
+import android.os.Bundle
+import android.view.View
 import androidx.fragment.app.Fragment
 import de.westnordost.streetcomplete.Prefs
 import de.westnordost.streetcomplete.R
@@ -11,7 +13,9 @@ import de.westnordost.streetcomplete.data.upload.UploadProgressListener
 import de.westnordost.streetcomplete.data.upload.UploadProgressSource
 import de.westnordost.streetcomplete.data.user.statistics.StatisticsSource
 import de.westnordost.streetcomplete.util.ktx.viewLifecycleScope
+import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
 import org.koin.android.ext.android.inject
 
 /** Fragment that shows the "star" with the number of solved quests */
@@ -25,6 +29,7 @@ class AnswersCounterFragment : Fragment(R.layout.fragment_answers_counter) {
     private val unsyncedChangesCountSource: UnsyncedChangesCountSource by inject()
 
     private val answersCounterView get() = view as AnswersCounterView
+    private var showCurrentWeek: Boolean = false
 
     private val uploadProgressListener = object : UploadProgressListener {
         override fun onStarted() { viewLifecycleScope.launch { updateProgress() } }
@@ -60,6 +65,21 @@ class AnswersCounterFragment : Fragment(R.layout.fragment_answers_counter) {
 
     /* --------------------------------------- Lifecycle ---------------------------------------- */
 
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+
+        showCurrentWeek = savedInstanceState?.getBoolean(SHOW_CURRENT_WEEK, false) ?: false
+        answersCounterView.showLabel = showCurrentWeek
+
+        answersCounterView.setOnClickListener {
+            showCurrentWeek = !showCurrentWeek
+            viewLifecycleScope.launch {
+                updateCount(false)
+                answersCounterView.showLabel = showCurrentWeek
+            }
+        }
+    }
+
     override fun onStart() {
         super.onStart()
 
@@ -81,6 +101,11 @@ class AnswersCounterFragment : Fragment(R.layout.fragment_answers_counter) {
         unsyncedChangesCountSource.removeListener(unsyncedChangesCountListener)
     }
 
+    override fun onSaveInstanceState(outState: Bundle) {
+        super.onSaveInstanceState(outState)
+        outState.putBoolean(SHOW_CURRENT_WEEK, showCurrentWeek)
+    }
+
     private val isAutosync: Boolean get() =
         Prefs.Autosync.valueOf(prefs.getString(Prefs.AUTOSYNC, "ON")!!) == Prefs.Autosync.ON
 
@@ -92,11 +117,18 @@ class AnswersCounterFragment : Fragment(R.layout.fragment_answers_counter) {
     private suspend fun updateCount(animated: Boolean) {
         /* if autosync is on, show the uploaded count + the to-be-uploaded count (but only those
            uploadables that will be part of the statistics, so no note stuff) */
-        val amount = statisticsSource.getEditCount() + if (isAutosync) unsyncedChangesCountSource.getSolvedCount() else 0
+        val editCount = withContext(Dispatchers.IO) {
+            if (showCurrentWeek) statisticsSource.getCurrentWeekEditCount() else statisticsSource.getEditCount()
+        }
+        val amount = editCount + if (isAutosync) unsyncedChangesCountSource.getSolvedCount() else 0
         answersCounterView.setUploadedCount(amount, animated)
     }
 
     private fun addCount(diff: Int, animate: Boolean) {
         answersCounterView.setUploadedCount(answersCounterView.uploadedCount + diff, animate)
     }
+
+    companion object {
+        private const val SHOW_CURRENT_WEEK = "showCurrentWeek"
+    }
 }
diff --git a/app/src/main/java/de/westnordost/streetcomplete/screens/main/controls/AnswersCounterView.kt b/app/src/main/java/de/westnordost/streetcomplete/screens/main/controls/AnswersCounterView.kt
index cbcefd027cf..7c7f5a8f0dd 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/screens/main/controls/AnswersCounterView.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/screens/main/controls/AnswersCounterView.kt
@@ -6,6 +6,7 @@ import android.view.LayoutInflater
 import android.view.animation.AccelerateDecelerateInterpolator
 import android.view.animation.DecelerateInterpolator
 import android.widget.RelativeLayout
+import androidx.core.view.isGone
 import androidx.core.view.isInvisible
 import de.westnordost.streetcomplete.databinding.ViewAnswersCounterBinding
 
@@ -30,6 +31,10 @@ class AnswersCounterView @JvmOverloads constructor(
             binding.progressView.isInvisible = !value
         }
 
+    var showLabel: Boolean
+        set(value) { binding.labelView.isGone = !value }
+        get() = binding.labelView.isGone
+
     fun setUploadedCount(uploadedCount: Int, animate: Boolean) {
         if (this.uploadedCount < uploadedCount && animate) {
             animateChange()
diff --git a/app/src/main/java/de/westnordost/streetcomplete/screens/user/profile/ProfileFragment.kt b/app/src/main/java/de/westnordost/streetcomplete/screens/user/profile/ProfileFragment.kt
index 9afc41290b1..2ac774598b9 100644
--- a/app/src/main/java/de/westnordost/streetcomplete/screens/user/profile/ProfileFragment.kt
+++ b/app/src/main/java/de/westnordost/streetcomplete/screens/user/profile/ProfileFragment.kt
@@ -6,6 +6,7 @@ import android.graphics.Bitmap
 import android.graphics.BitmapFactory
 import android.os.Bundle
 import android.view.View
+import android.widget.TextView
 import androidx.core.net.toUri
 import androidx.core.view.isGone
 import androidx.fragment.app.Fragment
@@ -14,7 +15,9 @@ import de.westnordost.streetcomplete.data.UnsyncedChangesCountSource
 import de.westnordost.streetcomplete.data.user.UserDataSource
 import de.westnordost.streetcomplete.data.user.UserLoginStatusController
 import de.westnordost.streetcomplete.data.user.UserUpdater
+import de.westnordost.streetcomplete.data.user.achievements.Achievement
 import de.westnordost.streetcomplete.data.user.achievements.AchievementsSource
+import de.westnordost.streetcomplete.data.user.statistics.CountryStatistics
 import de.westnordost.streetcomplete.data.user.statistics.StatisticsSource
 import de.westnordost.streetcomplete.databinding.FragmentProfileBinding
 import de.westnordost.streetcomplete.util.ktx.createBitmap
@@ -53,10 +56,10 @@ class ProfileFragment : Fragment(R.layout.fragment_profile) {
     }
     private val statisticsListener = object : StatisticsSource.Listener {
         override fun onAddedOne(type: String) {
-            viewLifecycleScope.launch { updateEditCountText() }
+            viewLifecycleScope.launch { updateEditCountTexts() }
         }
         override fun onSubtractedOne(type: String) {
-            viewLifecycleScope.launch { updateEditCountText() }
+            viewLifecycleScope.launch { updateEditCountTexts()  }
         }
         override fun onUpdatedAll() {
             viewLifecycleScope.launch { updateStatisticsTexts() }
@@ -68,6 +71,15 @@ class ProfileFragment : Fragment(R.layout.fragment_profile) {
             viewLifecycleScope.launch { updateDaysActiveText() }
         }
     }
+    private val achievementsListener = object : AchievementsSource.Listener {
+        override fun onAchievementUnlocked(achievement: Achievement, level: Int) {
+            viewLifecycleScope.launch { updateAchievementLevelsText() }
+        }
+
+        override fun onAllAchievementsUpdated() {
+            viewLifecycleScope.launch { updateAchievementLevelsText() }
+        }
+    }
     private val userListener = object : UserDataSource.Listener {
         override fun onUpdated() { viewLifecycleScope.launch { updateUserName() } }
     }
@@ -99,14 +111,15 @@ class ProfileFragment : Fragment(R.layout.fragment_profile) {
             userUpdater.addUserAvatarListener(userAvatarListener)
             statisticsSource.addListener(statisticsListener)
             unsyncedChangesCountSource.addListener(unsyncedChangesCountListener)
+            achievementsSource.addListener(achievementsListener)
 
             updateUserName()
             updateAvatar()
-            updateEditCountText()
+            updateEditCountTexts()
             updateUnpublishedEditsText()
             updateDaysActiveText()
-            updateGlobalRankText()
-            updateLocalRankText()
+            updateGlobalRankTexts()
+            updateLocalRankTexts()
             updateAchievementLevelsText()
         }
     }
@@ -117,6 +130,7 @@ class ProfileFragment : Fragment(R.layout.fragment_profile) {
         statisticsSource.removeListener(statisticsListener)
         userDataSource.removeListener(userListener)
         userUpdater.removeUserAvatarListener(userAvatarListener)
+        achievementsSource.removeListener(achievementsListener)
     }
 
     private fun updateUserName() {
@@ -130,14 +144,15 @@ class ProfileFragment : Fragment(R.layout.fragment_profile) {
     }
 
     private suspend fun updateStatisticsTexts() {
-        updateEditCountText()
+        updateEditCountTexts()
         updateDaysActiveText()
-        updateGlobalRankText()
-        updateLocalRankText()
+        updateGlobalRankTexts()
+        updateLocalRankTexts()
     }
 
-    private suspend fun updateEditCountText() {
+    private suspend fun updateEditCountTexts() {
         binding.editCountText.text = withContext(Dispatchers.IO) { statisticsSource.getEditCount().toString() }
+        binding.currentWeekEditCountText.text = withContext(Dispatchers.IO) { statisticsSource.getCurrentWeekEditCount().toString() }
     }
 
     private suspend fun updateUnpublishedEditsText() {
@@ -153,37 +168,62 @@ class ProfileFragment : Fragment(R.layout.fragment_profile) {
         binding.daysActiveText.background = LaurelWreathDrawable(resources, min(daysActive + 20, 100))
     }
 
-    private fun updateGlobalRankText() {
+    private fun updateGlobalRankTexts() {
+        updateGlobalRankText(
+            statisticsSource.rank,
+            binding.globalRankContainer,
+            binding.globalRankText
+        )
+
+        updateGlobalRankText(
+            statisticsSource.currentWeekRank,
+            binding.currentWeekGlobalRankContainer,
+            binding.currentWeekGlobalRankText
+        )
+    }
+
+    private fun updateGlobalRankText(rank: Int, container: View, circle: TextView ) {
+        container.isGone = rank <= 0 || statisticsSource.getEditCount() <= 100
+        circle.text = "#$rank"
+        circle.background = LaurelWreathDrawable(resources, getScaledGlobalRank(rank))
+    }
+
+    /** Translate the user's actual rank to a value from 0 (bad) to 100 (good) */
+    private fun getScaledGlobalRank(rank: Int): Int {
         // note that global rank merges multiple people with the same score
         // in case that 1000 people made 11 edits all will have the same rank (say, 3814)
         // in case that 1000 people made 10 edits all will have the same rank (in this case - 3815)
-        val rank = statisticsSource.rank
-        binding.globalRankContainer.isGone = rank <= 0 || statisticsSource.getEditCount() <= 100
-        binding.globalRankText.text = "#$rank"
         val rankEnoughForFullMarks = 1000
         val rankEnoughToStartGrowingReward = 3800
         val ranksAboveThreshold = max(rankEnoughToStartGrowingReward - rank, 0)
-        val scaledRank = (ranksAboveThreshold * 100.0 / (rankEnoughToStartGrowingReward - rankEnoughForFullMarks)).toInt()
-        binding.globalRankText.background = LaurelWreathDrawable(resources, min(scaledRank, 100))
-    }
-
-    private suspend fun updateLocalRankText() {
-        val statistics = withContext(Dispatchers.IO) {
-            statisticsSource.getCountryStatisticsOfCountryWithBiggestSolvedCount()
-        }
-        if (statistics == null) {
-            binding.localRankContainer.isGone = true
-        }
-        else {
-            val shouldShow = statistics.rank != null && statistics.rank > 0 && statistics.count > 50
-            val countryLocale = Locale("", statistics.countryCode)
-            binding.localRankContainer.isGone = !shouldShow
-            if (shouldShow) {
-                binding.localRankText.text = "#${statistics.rank}"
-                binding.localRankLabel.text = getString(R.string.user_profile_local_rank, countryLocale.displayCountry)
-                binding.localRankText.background = LaurelWreathDrawable(resources, min(100 - statistics.rank!!, 100))
-            }
-        }
+        return min(100, (ranksAboveThreshold * 100.0 / (rankEnoughToStartGrowingReward - rankEnoughForFullMarks)).toInt())
+    }
+
+    private suspend fun updateLocalRankTexts() {
+        updateLocalRankText(
+            withContext(Dispatchers.IO) { statisticsSource.getCountryStatisticsOfCountryWithBiggestSolvedCount() },
+            50,
+            binding.localRankContainer,
+            binding.localRankLabel,
+            binding.localRankText
+        )
+
+        updateLocalRankText(
+            withContext(Dispatchers.IO) { statisticsSource.getCurrentWeekCountryStatisticsOfCountryWithBiggestSolvedCount() },
+            5,
+            binding.currentWeekLocalRankContainer,
+            binding.currentWeekLocalRankLabel,
+            binding.currentWeekLocalRankText
+        )
+    }
+
+    private fun updateLocalRankText(statistics: CountryStatistics?, min: Int, container: View, label: TextView, circle: TextView) {
+        val rank = statistics?.rank ?: 0
+        container.isGone = statistics == null || rank <= 0 || statistics.count <= min
+        circle.text = "#$rank"
+        val countryLocale = Locale("", statistics?.countryCode ?: "")
+        label.text = getString(R.string.user_profile_local_rank, countryLocale.displayCountry)
+        circle.background = LaurelWreathDrawable(resources, min(100 - rank, 100))
     }
 
     private suspend fun updateAchievementLevelsText() {
diff --git a/app/src/main/res/drawable/space_24dp.xml b/app/src/main/res/drawable/space_24dp.xml
new file mode 100644
index 00000000000..261afd38d13
--- /dev/null
+++ b/app/src/main/res/drawable/space_24dp.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <size android:height="24dp" android:width="24dp"/>
+</shape>
diff --git a/app/src/main/res/layout/fragment_profile.xml b/app/src/main/res/layout/fragment_profile.xml
index dac8aff5624..677d9446b54 100644
--- a/app/src/main/res/layout/fragment_profile.xml
+++ b/app/src/main/res/layout/fragment_profile.xml
@@ -4,10 +4,6 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     xmlns:tools="http://schemas.android.com/tools"
-    android:paddingTop="@dimen/activity_vertical_margin"
-    android:paddingBottom="@dimen/activity_vertical_margin"
-    android:paddingStart="@dimen/activity_horizontal_margin"
-    android:paddingEnd="@dimen/activity_horizontal_margin"
     xmlns:app="http://schemas.android.com/apk/res-auto">
 
     <androidx.constraintlayout.widget.ConstraintLayout
@@ -26,7 +22,13 @@
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:orientation="vertical"
-                android:paddingBottom="48dp">
+                android:layout_marginTop="@dimen/activity_vertical_margin"
+                android:layout_marginBottom="@dimen/activity_vertical_margin"
+                android:layout_marginStart="@dimen/activity_horizontal_margin"
+                android:layout_marginEnd="@dimen/activity_horizontal_margin"
+                android:divider="@drawable/space_24dp"
+                android:paddingBottom="64dp"
+                android:showDividers="middle">
 
                 <RelativeLayout
                     android:id="@+id/profileContainer"
@@ -104,15 +106,40 @@
 
                 </RelativeLayout>
 
+                <RelativeLayout
+                    android:id="@+id/buttonsContainer"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content">
+
+                    <Button
+                        android:id="@+id/profileButton"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:layout_alignParentStart="true"
+                        app:icon="@drawable/ic_open_in_browser_24dp"
+                        android:layout_centerHorizontal="true"
+                        android:text="@string/osm_profile" />
+
+                    <Button
+                        android:id="@+id/logoutButton"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:layout_alignParentEnd="true"
+                        android:text="@string/user_logout"
+                        style="@style/Widget.MaterialComponents.Button.OutlinedButton"/>
+
+                </RelativeLayout>
+
+                <View
+                    android:layout_width="match_parent"
+                    android:layout_height="1dp"
+                    android:background="@color/divider"/>
+
                 <com.google.android.flexbox.FlexboxLayout
                     android:id="@+id/badgesContainer"
                     android:layout_width="match_parent"
                     android:layout_height="wrap_content"
                     android:orientation="horizontal"
-                    android:layout_marginTop="16dp"
-                    app:layout_constraintEnd_toEndOf="parent"
-                    app:layout_constraintStart_toStartOf="parent"
-                    app:layout_constraintTop_toBottomOf="@id/profileContainer"
                     app:flexWrap="wrap"
                     app:showDivider="middle"
                     app:dividerDrawableHorizontal="@drawable/space_16dp"
@@ -261,36 +288,128 @@
 
                 </com.google.android.flexbox.FlexboxLayout>
 
-            </LinearLayout>
+                <LinearLayout
+                    android:id="@+id/currentWeekSolvedQuestsContainer"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:divider="@drawable/space_4dp"
+                    android:gravity="center_vertical"
+                    android:orientation="horizontal"
+                    android:showDividers="middle">
 
-        </ScrollView>
+                    <TextView
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:gravity="center_vertical"
+                        android:textAlignment="gravity"
+                        android:textAppearance="@style/TextAppearance.Title"
+                        android:text="@string/user_profile_current_week_title" />
 
-        <RelativeLayout
-            android:id="@+id/buttonsContainer"
-            android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            app:layout_constraintEnd_toEndOf="parent"
-            app:layout_constraintStart_toStartOf="parent"
-            app:layout_constraintBottom_toBottomOf="parent">
-
-            <Button
-                android:id="@+id/profileButton"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_alignParentStart="true"
-                app:icon="@drawable/ic_open_in_browser_24dp"
-                android:layout_centerHorizontal="true"
-                android:text="@string/osm_profile" />
-
-            <Button
-                android:id="@+id/logoutButton"
-                android:layout_width="wrap_content"
-                android:layout_height="wrap_content"
-                android:layout_alignParentEnd="true"
-                android:text="@string/user_logout"
-                style="@style/Widget.MaterialComponents.Button"/>
+                    <ImageView
+                        android:layout_width="24dp"
+                        android:layout_height="24dp"
+                        android:src="@drawable/ic_star_48dp" />
+
+                    <TextView
+                        android:id="@+id/currentWeekEditCountText"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:gravity="center_vertical"
+                        android:maxLines="1"
+                        android:textAlignment="gravity"
+                        android:textAppearance="@style/TextAppearance.Title"
+                        tools:text="1021" />
+
+                </LinearLayout>
+
+                <com.google.android.flexbox.FlexboxLayout
+                    android:id="@+id/currentWeekBadgesContainer"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:orientation="horizontal"
+                    app:flexWrap="wrap"
+                    app:showDivider="middle"
+                    app:dividerDrawableHorizontal="@drawable/space_16dp"
+                    app:dividerDrawableVertical="@drawable/space_8dp"
+                    app:showDividerVertical="middle"
+                    app:showDividerHorizontal="middle"
+                    app:alignContent="stretch">
+
+                    <RelativeLayout
+                        android:id="@+id/currentWeekLocalRankContainer"
+                        android:layout_width="84dp"
+                        android:layout_height="wrap_content">
+
+                        <TextView
+                            android:id="@+id/currentWeekLocalRankText"
+                            android:layout_width="84dp"
+                            android:layout_height="84dp"
+                            android:background="@drawable/background_inverted_text_circle"
+                            android:gravity="center"
+                            android:padding="8dp"
+                            android:maxLines="1"
+                            android:textAppearance="@style/TextAppearance.Title"
+                            android:layout_centerHorizontal="true"
+                            app:autoSizeTextType="uniform"
+                            app:autoSizeMaxTextSize="22dp"
+                            app:autoSizeMinTextSize="12dp"
+                            android:textColor="@color/background"
+                            tools:text="#1"/>
+
+                        <TextView
+                            android:id="@+id/currentWeekLocalRankLabel"
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:layout_centerHorizontal="true"
+                            android:gravity="center"
+                            android:textAppearance="@style/TextAppearance.Body"
+                            android:labelFor="@id/currentWeekLocalRankText"
+                            android:layout_below="@+id/currentWeekLocalRankText"
+                            android:layout_marginTop="4dp"
+                            android:text="@string/user_profile_local_rank"
+                            tools:text="Rank in\nUnited States of America"/>
+
+                    </RelativeLayout>
+
+                    <RelativeLayout
+                        android:id="@+id/currentWeekGlobalRankContainer"
+                        android:layout_width="84dp"
+                        android:layout_height="wrap_content">
+
+                        <TextView
+                            android:id="@+id/currentWeekGlobalRankText"
+                            android:layout_width="84dp"
+                            android:layout_height="84dp"
+                            android:background="@drawable/background_inverted_text_circle"
+                            android:gravity="center"
+                            android:padding="8dp"
+                            android:maxLines="1"
+                            android:textAppearance="@style/TextAppearance.Title"
+                            android:layout_centerHorizontal="true"
+                            app:autoSizeTextType="uniform"
+                            app:autoSizeMaxTextSize="22dp"
+                            app:autoSizeMinTextSize="12dp"
+                            android:textColor="@color/background"
+                            tools:text="#123456"/>
+
+                        <TextView
+                            android:layout_width="match_parent"
+                            android:layout_height="wrap_content"
+                            android:layout_centerHorizontal="true"
+                            android:gravity="center"
+                            android:textAppearance="@style/TextAppearance.Body"
+                            android:labelFor="@id/currentWeekGlobalRankText"
+                            android:layout_below="@id/currentWeekGlobalRankText"
+                            android:layout_marginTop="4dp"
+                            android:text="@string/user_profile_global_rank"/>
 
-        </RelativeLayout>
+                    </RelativeLayout>
+
+                </com.google.android.flexbox.FlexboxLayout>
+
+            </LinearLayout>
+
+        </ScrollView>
 
     </androidx.constraintlayout.widget.ConstraintLayout>
 
diff --git a/app/src/main/res/layout/view_answers_counter.xml b/app/src/main/res/layout/view_answers_counter.xml
index 18ba6ecbc14..d42dc61f68d 100644
--- a/app/src/main/res/layout/view_answers_counter.xml
+++ b/app/src/main/res/layout/view_answers_counter.xml
@@ -5,8 +5,7 @@
     android:layout_width="wrap_content"
     android:layout_height="wrap_content"
     android:clipChildren="false"
-    android:clipToPadding="false"
-    tools:background="#666">
+    android:clipToPadding="false">
 
     <ProgressBar
         android:id="@+id/progressView"
@@ -30,25 +29,45 @@
         android:src="@drawable/ic_star_white_shadow_32dp"
         tools:ignore="RtlHardcoded" />
 
-    <TextView
-        android:id="@+id/textView"
+    <RelativeLayout
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
-        android:gravity="center"
         android:layout_toRightOf="@id/iconView"
+        android:layout_marginLeft="-12dp"
         android:layout_centerVertical="true"
-        android:textAppearance="@style/TextAppearance.BigBody"
-        android:textStyle="bold"
-        android:textColor="#fff"
-        android:paddingLeft="16dp"
-        android:paddingRight="16dp"
-        android:layout_marginLeft="-8dp"
-        tools:scaleX="1.6"
-        tools:scaleY="1.6"
-        android:shadowColor="#000"
-        android:shadowRadius="8"
-        android:shadowDy="2"
-        tools:text="1583"
-        tools:ignore="RtlHardcoded"/>
+        tools:ignore="RtlHardcoded">
+
+        <TextView
+            android:id="@+id/labelView"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/user_profile_current_week_title"
+            android:paddingLeft="16dp"
+            android:paddingRight="16dp"
+            android:textColor="#fff"
+            android:shadowColor="#000"
+            android:shadowRadius="6"
+            android:shadowDy="2"
+            android:textStyle="bold"
+            tools:visibility="visible"
+            android:visibility="gone"/>
+
+        <TextView
+            android:id="@+id/textView"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:gravity="center"
+            android:layout_below="@+id/labelView"
+            android:textAppearance="@style/TextAppearance.BigBody"
+            android:textStyle="bold"
+            android:textColor="#fff"
+            android:paddingLeft="16dp"
+            android:paddingRight="16dp"
+            android:shadowColor="#000"
+            android:shadowRadius="6"
+            android:shadowDy="2"
+            tools:text="1583"/>
+
+    </RelativeLayout>
 
 </merge>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 7f636283b95..9e5ee1dcdf6 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -357,6 +357,7 @@ Before uploading your changes, the app checks with a &lt;a href=\"https://www.we
     <string name="user_profile_local_rank">Rank in\n%s</string>
     <string name="user_profile_days_active">Days\nactive</string>
     <string name="user_profile_achievement_levels">Achievement\nlevels</string>
+    <string name="user_profile_current_week_title">This week</string>
 
     <string name="user_login">Login</string>
     <string name="user_logout">Logout</string>


Test patch:
diff --git a/app/src/androidTest/java/de/westnordost/streetcomplete/data/user/statistics/CountryStatisticsDaoTest.kt b/app/src/androidTest/java/de/westnordost/streetcomplete/data/user/statistics/CountryStatisticsDaoTest.kt
index 67b506b978f..5d6de8c1833 100644
--- a/app/src/androidTest/java/de/westnordost/streetcomplete/data/user/statistics/CountryStatisticsDaoTest.kt
+++ b/app/src/androidTest/java/de/westnordost/streetcomplete/data/user/statistics/CountryStatisticsDaoTest.kt
@@ -9,7 +9,7 @@ class CountryStatisticsDaoTest : ApplicationDbTestCase() {
     private lateinit var dao: CountryStatisticsDao
 
     @Before fun createDao() {
-        dao = CountryStatisticsDao(database)
+        dao = CountryStatisticsDao(database, CountryStatisticsTables.NAME)
     }
 
     @Test fun addAndSubtract() {
diff --git a/app/src/androidTest/java/de/westnordost/streetcomplete/data/user/statistics/EditTypeStatisticsDaoTest.kt b/app/src/androidTest/java/de/westnordost/streetcomplete/data/user/statistics/EditTypeStatisticsDaoTest.kt
index e4167e4d454..2a290377d0c 100644
--- a/app/src/androidTest/java/de/westnordost/streetcomplete/data/user/statistics/EditTypeStatisticsDaoTest.kt
+++ b/app/src/androidTest/java/de/westnordost/streetcomplete/data/user/statistics/EditTypeStatisticsDaoTest.kt
@@ -9,7 +9,7 @@ class EditTypeStatisticsDaoTest : ApplicationDbTestCase() {
     private lateinit var daoType: EditTypeStatisticsDao
 
     @Before fun createDao() {
-        daoType = EditTypeStatisticsDao(database)
+        daoType = EditTypeStatisticsDao(database, EditTypeStatisticsTables.NAME)
     }
 
     @Test fun getZero() {
diff --git a/app/src/test/java/de/westnordost/streetcomplete/data/user/statistics/StatisticsControllerTest.kt b/app/src/test/java/de/westnordost/streetcomplete/data/user/statistics/StatisticsControllerTest.kt
index f56c78ce431..1974b24f974 100644
--- a/app/src/test/java/de/westnordost/streetcomplete/data/user/statistics/StatisticsControllerTest.kt
+++ b/app/src/test/java/de/westnordost/streetcomplete/data/user/statistics/StatisticsControllerTest.kt
@@ -19,6 +19,8 @@ class StatisticsControllerTest {
 
     private lateinit var editTypeStatisticsDao: EditTypeStatisticsDao
     private lateinit var countryStatisticsDao: CountryStatisticsDao
+    private lateinit var currentWeekEditTypeStatisticsDao: EditTypeStatisticsDao
+    private lateinit var currentWeekCountryStatisticsDao: CountryStatisticsDao
     private lateinit var countryBoundaries: CountryBoundaries
     private lateinit var prefs: SharedPreferences
     private lateinit var loginStatusSource: UserLoginStatusSource
@@ -33,6 +35,8 @@ class StatisticsControllerTest {
     @Before fun setUp() {
         editTypeStatisticsDao = mock()
         countryStatisticsDao = mock()
+        currentWeekEditTypeStatisticsDao = mock()
+        currentWeekCountryStatisticsDao = mock()
         countryBoundaries = mock()
         prefs = mock()
         on(prefs.edit()).thenReturn(mock())
@@ -48,7 +52,9 @@ class StatisticsControllerTest {
         }
 
         statisticsController = StatisticsController(
-            editTypeStatisticsDao, countryStatisticsDao, ft, prefs, loginStatusSource
+            editTypeStatisticsDao, countryStatisticsDao,
+            currentWeekEditTypeStatisticsDao, currentWeekCountryStatisticsDao,
+            ft, prefs, loginStatusSource
         )
         statisticsController.addListener(listener)
     }
@@ -67,6 +73,8 @@ class StatisticsControllerTest {
 
         verify(editTypeStatisticsDao).addOne("TestQuestTypeA")
         verify(countryStatisticsDao).addOne("US")
+        verify(currentWeekEditTypeStatisticsDao).addOne("TestQuestTypeA")
+        verify(currentWeekCountryStatisticsDao).addOne("US")
         verify(listener).onAddedOne(questA)
     }
 
@@ -76,6 +84,7 @@ class StatisticsControllerTest {
         statisticsController.addOne(questA, p(0.0, 0.0))
 
         verify(editTypeStatisticsDao).addOne("TestQuestTypeA")
+        verify(currentWeekEditTypeStatisticsDao).addOne("TestQuestTypeA")
         verify(listener).onAddedOne(questA)
         verify(listener).onUpdatedDaysActive()
     }
@@ -86,6 +95,8 @@ class StatisticsControllerTest {
 
         verify(editTypeStatisticsDao).subtractOne("TestQuestTypeA")
         verify(countryStatisticsDao).subtractOne("US")
+        verify(currentWeekEditTypeStatisticsDao).subtractOne("TestQuestTypeA")
+        verify(currentWeekCountryStatisticsDao).subtractOne("US")
         verify(listener).onSubtractedOne(questA)
     }
 
@@ -95,6 +106,7 @@ class StatisticsControllerTest {
         statisticsController.subtractOne(questA, p(0.0, 0.0))
 
         verify(editTypeStatisticsDao).subtractOne("TestQuestTypeA")
+        verify(currentWeekEditTypeStatisticsDao).subtractOne("TestQuestTypeA")
         verify(listener).onSubtractedOne(questA)
         verify(listener).onUpdatedDaysActive()
     }
@@ -107,9 +119,12 @@ class StatisticsControllerTest {
 
         verify(editTypeStatisticsDao).clear()
         verify(countryStatisticsDao).clear()
+        verify(currentWeekCountryStatisticsDao).clear()
+        verify(currentWeekEditTypeStatisticsDao).clear()
         verify(editor).remove(Prefs.USER_DAYS_ACTIVE)
         verify(editor).remove(Prefs.IS_SYNCHRONIZING_STATISTICS)
         verify(editor).remove(Prefs.USER_GLOBAL_RANK)
+        verify(editor).remove(Prefs.USER_GLOBAL_RANK_CURRENT_WEEK)
         verify(editor).remove(Prefs.USER_LAST_TIMESTAMP_ACTIVE)
         verify(listener).onCleared()
     }
@@ -129,6 +144,15 @@ class StatisticsControllerTest {
             ),
             rank = 999,
             daysActive = 333,
+            currentWeekRank = 111,
+            currentWeekTypes = listOf(
+                EditTypeStatistics(questA, 321),
+                EditTypeStatistics(questB, 33),
+            ),
+            currentWeekCountries = listOf(
+                CountryStatistics("AT", 999, 88),
+                CountryStatistics("IT", 99, null),
+            ),
             lastUpdate = 9999999,
             isAnalyzing = false
         ))
@@ -140,9 +164,18 @@ class StatisticsControllerTest {
             CountryStatistics("DE", 12, 5),
             CountryStatistics("US", 43, null),
         ))
+        verify(currentWeekEditTypeStatisticsDao).replaceAll(mapOf(
+            "TestQuestTypeA" to 321,
+            "TestQuestTypeB" to 33
+        ))
+        verify(currentWeekCountryStatisticsDao).replaceAll(listOf(
+            CountryStatistics("AT", 999, 88),
+            CountryStatistics("IT", 99, null),
+        ))
         verify(editor).putInt(Prefs.USER_DAYS_ACTIVE, 333)
         verify(editor).putBoolean(Prefs.IS_SYNCHRONIZING_STATISTICS, false)
         verify(editor).putInt(Prefs.USER_GLOBAL_RANK, 999)
+        verify(editor).putInt(Prefs.USER_GLOBAL_RANK_CURRENT_WEEK, 111)
         verify(editor).putLong(Prefs.USER_LAST_TIMESTAMP_ACTIVE, 9999999)
         verify(listener).onUpdatedAll()
     }
diff --git a/app/src/test/java/de/westnordost/streetcomplete/data/user/statistics/StatisticsParserTest.kt b/app/src/test/java/de/westnordost/streetcomplete/data/user/statistics/StatisticsParserTest.kt
index 0cfe9c54bd2..bbe8a6cd53b 100644
--- a/app/src/test/java/de/westnordost/streetcomplete/data/user/statistics/StatisticsParserTest.kt
+++ b/app/src/test/java/de/westnordost/streetcomplete/data/user/statistics/StatisticsParserTest.kt
@@ -22,10 +22,20 @@ class StatisticsParserTest {
                 CountryStatistics("DE", 8, null),
                 CountryStatistics("US", 7, 123),
             ),
-            2345,
-            78,
-            Instant.parse("2007-12-03T10:15:30+01:00").toEpochMilliseconds(),
-            false
+            rank = 2345,
+            daysActive = 78,
+            currentWeekRank = 3,
+            currentWeekTypes = listOf(
+                EditTypeStatistics(questA, 9),
+                EditTypeStatistics(questB, 99),
+                EditTypeStatistics(questC, 999),
+            ),
+            currentWeekCountries = listOf(
+                CountryStatistics("AT", 5, 666),
+                CountryStatistics("IT", 4, null),
+            ),
+            lastUpdate = Instant.parse("2007-12-03T10:15:30+01:00").toEpochMilliseconds(),
+            isAnalyzing = false
         ),
         StatisticsParser(listOf("TestQuestTypeCAlias" to "TestQuestTypeC")).parse("""
         {
@@ -42,6 +52,19 @@ class StatisticsParserTest {
                 "US": "123",
             },
             "rank": "2345",
+            "currentWeekRank": "3",
+            "currentWeekQuestTypes": {
+                "TestQuestTypeA": "9",
+                "TestQuestTypeB": "99",
+                "TestQuestTypeCAlias": "999",
+            },
+            "currentWeekCountries": {
+                "IT": 4,
+                "AT": 5,
+            },
+            "currentWeekCountryRanks": {
+                "AT": 666
+            },
             "daysActive": "78",
             "lastUpdate": "2007-12-03T10:15:30+01:00",
             "isAnalyzing": "false"